Jonesy的文章

  • 中山大学智慧健康服务平台应用开发-Broadcast使用

    一、实验题目中山大学智慧健康服务平台应用开发
    二、实现内容2.1 Broadcast 使用2.1.1 实验目的
    掌握 Broadcast 编程基础
    掌握动态注册 Broadcast 和静态注册 Broadcast
    掌握Notification 编程基础
    掌握 EventBus 编程基础

    2.1.2 实验内容在之前的基础上,实现静态广播、动态广播两种改变Notification 内容的方法。
    要求在启动应用时,会有通知产生,随机推荐一个食品

    点击通知跳转到所推荐食品的详情界面

    点击收藏图标,会有对应通知产生,并通过Eventbus在收藏列表更新数据

    点击通知返回收藏列表

    实现方式要求:启动页面的通知由静态广播产生,点击收藏图标的通知由动态广播产生。
    2.1.3 验收内容
    静态广播:启动应用是否有随机推荐食品的通知产生。点击通知是否正确跳转到所推荐食品的详情界面
    动态广播:点击收藏后是否有提示食品已加入收藏列表的通知产生。同时注意设置launchMode。点击通知是否跳转到收藏列表
    Eventbus:点击收藏列表图标是否正确添加食品到收藏列表。每点击一次,添加对应的一个食品到收藏列表并产生一条通知

    三、实验结果3.1 实验截图下图为打开app后,产生一个推荐食品的通知

    下图为点击该通知,会跳转至食物详情页面。点击收藏按钮时,产生收藏的通知

    下图为点击收藏通知,跳转至收藏列表页面

    3.2 实验步骤以及关键代码3.2.1 利用静态广播实现今日推荐功能在AndroidManifest.xml注册静态广播接受方其中StaticReceiver为类名
    <receiver android:name=".StaticReceiver"> <intent-filter> <action android:name="com.example.asus.health.MyStaticFilter" /> </intent-filter> </receiver>
    实现StaticReceiver类,重构onReceive函数其中要根据intent的action来确定是否接受该广播的内容,来实现功能,而需要实现的包括一个notification的弹出以及点击它跳转到详情页面。
    notification部分由builder的设置函数来设置名字,内容,等等,由NotificationManager来发出该notification。
    点击后跳转的功能则需要给builder设置一个ContentIntent,这个intent为PeddingIntent,即不会马上跳转,而是需要等待用户的操作。它的构造函数传递了一个普通的intent,而这个intent是携带了所需的数据来生成详情页面。
    public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(STATICACTION)){ Bundle bundle = intent.getExtras(); //TODO:添加Notification部分 Notification.Builder builder = new Notification.Builder(context); //跳回主页面 Intent intent2 = new Intent(context,Details.class); Bundle bundle2 = new Bundle(); String s[] = new String [5]; s[0] = ((MyCollection)bundle.getSerializable("collect")).getName(); s[1] = ((MyCollection)bundle.getSerializable("collect")).getMaterial(); s[2] = ((MyCollection)bundle.getSerializable("collect")).getType(); s[3] = ((MyCollection)bundle.getSerializable("collect")).getContent(); s[4] = ((MyCollection)bundle.getSerializable("collect")).getIs_star()?"yes":"no"; bundle2.putStringArray("msg",s); intent2.putExtras(bundle2); PendingIntent contentIntent = PendingIntent.getActivity( context, 0, intent2, PendingIntent.FLAG_UPDATE_CURRENT); //对Builder进行配置 builder.setContentTitle("今日推荐") //设置通知栏标题:发件人 .setContentText(((MyCollection)bundle.getSerializable("collect")).getName()) //设置通知栏显示内容:短信内容 .setTicker("您有一条新消息") //通知首次出现在通知栏,带上升动画效果的 .setSmallIcon(R.mipmap.empty_star) //设置通知小ICON 空星 .setContentIntent(contentIntent) //传递内容 .setAutoCancel(true); //设置这个标志当用户单击面板就可以让通知将自动取消 //获取状态通知栏管理 NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); //绑定Notification,发送通知请求 Notification notify = builder.build(); manager.notify(0,notify); } }
    在FoodList主页面onCreat时生成广播注意action的字符串要与上面的Reciver的相同,不然无法正确接受广播,随机数则是返回一个0到n-1的整数表示随机生成一个推荐食物,然后将所需数据放入intent,通过sendBroadcast函数发送该广播。
    //打开应用时,发送一个静态广播 private void boardcastforOpen(int n){ final String STATICACTION = "com.example.asus.health.MyStaticFilter"; Random random = new Random(); int num = random.nextInt(n); //返回一个0到n-1的整数 Intent intentBroadcast = new Intent(STATICACTION); //定义Intent Log.i("se",getPackageName()); Bundle bundles = new Bundle(); bundles.putSerializable("collect", data2.get(num)); intentBroadcast.putExtras(bundles); sendBroadcast(intentBroadcast); }
    3.2.2 利用动态广播实现收藏信息提示实现广播接受器DynamicReceiver类与静态Receiver的实现过程差不多,一样是实现builder,然后放置peddingIntent,这里就不再重复放代码。唯一的不同点在于,它所要跳回的是收藏夹页面,即FoodList主页面,这里要对intent设置flag,否则无法在foodlist中get到新的intent。
    //跳回收藏夹 Intent intent2 = new Intent(context,FoodList.class); Bundle bundle2 = new Bundle(); bundle2.putString("tag","collect"); intent2.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent2.putExtras(bundle2);
    对详情页面的收藏事件进行处理在监听器中设置发送广播的intent,当按下收藏后会发出广播,并传递参数。其中还使用了eventbus来传递收藏的数据。
    //处理收藏按钮 final ImageView collect_but = findViewById(R.id.collect); collect_but.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v){ temp.setIs_collected(true); Toast.makeText(Details.this, "已收藏",Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new MessageEvent(temp)); //发送广播 Intent intentBroadcast = new Intent(); intentBroadcast.setAction(DYNAMICACTION); sendBroadcast(intentBroadcast); } });
    在FoodList注册动态接收器以及注销动态接收器注意分别要在onCreate函数以及onDestroy函数中实现注册与注销 。
    3.2.3 使用EventBus来实现数据的传输在这一点上,要改进上一周实验的代码,不再需要点击返回按钮利用setResult以及onActivityResult两个函数来返回信息。而是通过eventbus的订阅发布模式。
    在FoodList来注册订阅者,订阅消息。而在Detail来发布信息。其中onMessageEvent函数用于收到发布消息后,来调用之前的接口函数刷新列表。
    发布消息就是上面点击收藏按钮后 EventBus.getDefault().post(new MessageEvent(temp));
    //注册订阅者(注册收藏列表所在Activity为订阅者) EventBus.getDefault().register(this); @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(MessageEvent event) { Log.i("hello","this is eventbus."); MyCollection mc = event.getCollection(); refreshList(mc,simpleAdapter); }
    3.2.4 从详情跳转回收藏夹这里由于收藏夹FoodList为经常返回的页面,故这里使用了android:launchMode=”singleInstance”,即不让它重复创建新的活动。
    所以再get我的返回intent时是拿不到新的intent的,这里需要重写onNewIntent函数,而且接收新的intent要在onResume中。
    这里要求要显示收藏夹页面,所以要将食物列表隐藏起来。
    @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); setIntent(intent); } @Override protected void onResume(){ super.onResume(); //处理跳转 Bundle bundle=this.getIntent().getExtras(); if(bundle != null) { String str = bundle.getString("tag"); Log.i("resume2",str); if (str.equals("collect")) { findViewById(R.id.recyclerView).setVisibility(View.GONE); findViewById(R.id.listView).setVisibility(View.VISIBLE);//设置Favourite可见 tag_for_foodlist = false; f_but.setImageResource(R.mipmap.mainpage); } } }
    3.3 实验遇到的困难以及解决思路3.3.1 在安卓8.0版本中无法使用静态接收器,发送广播后,无法成功接收方法一:解决这个问题,需要给receiver设置component,给予它的包名以及类名。
    intent.setComponent(new ComponentName(getPackageName(),getPackageName()+".xxxxReceiver"));
    方法二:下载新的虚拟机,使用安卓7.0版本,则可以顺利接收静态广播。
    3.3.2 使用EventBus时候,FoodList主页面无法得到post的信息我按部就班地在Detail页面收藏按钮post,在FoodList订阅消息却毫无反应。首先,我认为是我的接收函数写错了,没有订阅到信息。通过Log.i发现确实没有进入到onMessageEvent函数中,于是对这个问题进行了查阅。网上有推荐使用stickyPost的,怀疑原因出在信息接收发生在创建接收者之前,但显然与函数执行顺序不符,它是先来到了主页面,所以必然创建了receiver。
    经过大半个小时的查找发现是,post传的参数错误,并没有生成MessageEvent,而是错误地直接传递了数据包。
    //错误EventBus.getDefault().post(temp);//正确EventBus.getDefault().post(new MessageEvent(temp));
    3.3.3 从收藏通知返回主页面时候,出现无法拿到intent的情况由于我是在动态接收方的builder绑定了Peddingintent,当点击通知,应该要返回这个intent到主页面,然而主页面所获取的intent是空值。这一点让我怀疑了很久,问了同学才得知,这是声明了singleInstance的问题。
    比如说在一个应用中A activity 跳转至 B activity 在跳转至 C activity 然后C做了一定的操作之后再返回A 界面。这样在A activity的启动模式设置为singleTask后。C界面跳转至A界面时,就会去判断栈内是否有改Activity实例,如果有就直接执行A界面的onNewIntent()方法,我们就可以把逻辑处理放在改生命周期方法中,如果没有就会走Activity的oncrate方法去创建实例。
    所以这里需要重写onNewIntent来获取新的intent,而不是直接传递旧intent导致错误。
    @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); setIntent(intent); }
    四、实验思考及感想这次实验需要在安卓8.0与安卓7.0之间权衡,有些属性方法已经在8.0版本出现了变化,所以当使用错误,出现奇怪的现象时,第一步先检查自己的代码逻辑有否问题,第二步就是要查阅是否存在版本的兼容性问题产生了这些错误。这次作业就是如此,关于广播的实现,个人还是喜欢动态广播,不需要再静态注册在manifest中,代码也更加简便。
    对于不同活动之间的传输,使用EventBus比之前的intent更加方便,减轻了耦合性,不用经常记住,哪个intent返回哪里,所以这次我也修改了不少前面实验使用intent的代码。除此之外,充分理解信息传输还需要理解一下活动的存活过程,什么时候调用onCreat,什么时候使用onResume。
    1  留言 2019-07-17 23:38:40
  • 中山大学智慧健康服务平台应用开发-基本的UI界面设计

    一、实验题目实验一: 中山大学智慧健康服务平台应用开发
    二、实现内容2.1 基本的UI界面设计实现一个Android应用,界面呈现如图中的效果。

    2.1.1 要求
    该界面为应用启动后看到的第一个界面
    各控件的要求

    标题字体大小20sp,与顶部距离20dp,居中图片与上下控件的间距均为20dp,居中输入框整体距左右屏幕各间距20dp,内容(包括提示内容)如图所示,内容字体大小18sp按钮与输入框间距10dp,文字大小18sp。按钮背景框左右边框与文字间距10dp,上下边框与文字间距5dp,圆角半径180dp,背景色为#3F51B5四个单选按钮整体居中,与输入框间距10dp,字体大小18sp,各个单选按钮之间间距10dp,默认选中的按钮为第一个

    2.1.2 使用的组件TextView、EditText、ConstraintLayout、Button、ImageView、RadioGroup、RadioButton。实现一个Android应用,界面呈现如图中的效果。
    2.1.3 验收内容
    各控件的位置,间距,字体大小等属性与要求无误
    图片大小不作为验收内容给之一

    2.2 事件处理
    2.2.1 要求
    该界面为应用启动后看到的第一个界面
    各控件处理的要求

    点击搜索按钮
    如果搜索内容为空,弹出Toast信息“搜索内容不能为空”如果搜索内容为“Health”,根据选中的RadioButton项弹出如下对话框点击“确定”,弹出Toast信息——对话框“确定”按钮被点击点击“取消”,弹出Toast 信息——对话框“取消”按钮被点击否则弹出如下对话框,对话框点击效果同上

    RadioButton选择项切换:选择项切换之后,弹出Toast信息“XX被选中”,例如从图片切换到视频,弹出Toast信息“视频被选中”

    2.2.2 验收内容
    布局是否正常
    搜索内容为空时,提示是否正常
    输入搜索内容后,点击搜索按钮是否能根据不同的搜索内容显示相应的弹出框,以及弹出框内容是否符合要求
    点击弹出框的相应按钮是否能提示正确的内容
    RadioButton切换时,提示是否正常

    2.3 Intent、Bundle的使用以及RecyclerView、ListView的应用本次实验模拟实现一个健康食品列表,有两个界面,第一个界面用于呈现食品列表如下所示。

    数据在”manual/素材”目录下给出。
    点击右下方的悬浮按钮可以切换到收藏夹。

    上面两个列表点击任意一项后,可以看到详细的信息:

    2.3.1 UI要求食品列表
    每一项为一个圆圈和一个名字,圆圈和名字都是垂直居中。圆圈内的内容是该食品的种类,内容要处于圆圈的中心,颜色为白色。食品名字为黑色,圆圈颜色自定义,只需能看见圆圈内的内容即可。
    收藏夹
    与食品列表相似。
    食品详情界面
    界面顶部

    顶部占整个界面的1/3。每个食品详情的顶部颜色在数据中已给出。返回图标处于这块区域的左上角,食品名字处于左下角,星标处于右下角,边距可以自己设置。 返回图标与名字左对齐,名字与星标底边对齐。 建议用RelativeLayout实现,以熟悉RelativeLayout的使用。
    界面中部

    使用的黑色argb编码值为#D5000000,稍微偏灰色的“富含”“蛋白质”的argb编码值为#8A000000。”更多资料”一栏上方有一条分割线,argb编码值为#1E000000。右边收藏符号的左边也有一条分割线,要求与收藏符号高度一致,垂直居中。字体大小自定。”更多资料”下方分割线高度自定。这部分所有的分割线argb编码值都是#1E000000。
    界面底部

    使用的黑色argb编码值为#D5000000。
    标题栏
    两个界面的标题栏都需要去掉。
    2.3.2 功能要求使用RecyclerView实现食品列表。点击某个食品会跳转到该食品的详情界面,呈现该食品的详细信息。长按列表中某个食品会删除该食品,并弹出Toast,提示 “删除XX” 。
    点击右下方的FloatingActionButton,从食品列表切换到收藏夹或从收藏夹切换到食品列表,并且该按钮的图片作出相应改变。
    使用ListView实现收藏夹。点击收藏夹的某个食品会跳转到食品详情界面,呈现该食品的详细信息。长按收藏夹中的某个食品会弹出对话框询问是否移出该食品,点击确定则移除该食品,点击取消则对话框消失。
    商品详情界面中点击返回图标会返回上一层。点击星标会切换状态,如果原本是空心星星,则会变成实心星星;原本是实心星星,则会变成空心星星。点击收藏图表则将该食品添加到收藏夹并弹出Toast提示 “已收藏” 。
    三、实验结果3.1 基本的UI界面设计与基础事件处理3.1.1 实验截图切换按钮时候,显示当前切换到的按钮名字,如下图,视频被选中:

    搜索Health关键词时,显示对话框搜索成功:

    搜索其他关键词,无法正确搜索,显示搜索错误对话框:

    点击取消按钮时,显示toast取消被单击:

    3.1.2 实验步骤以及关键代码这个实验前两部分包括简单的UI设计以及UI的交互。
    首先,我们当然要从UI的构建开始。
    1.插入标题以及图片这里应用到了TextView以及ImageView两个控件。由于本次的ui是使用ConstraintLayout布局,所以必须对每一个控件设置左右上下分别对齐什么。故要利用app:layout_constraintLeft_toLeftOf 等属性,表示该组件的左边对齐于xx的左边,这里的textview就要与parent即整个页面的左边对齐,然后设置居中。宽度,大小就根据实验要求来设置,而id是用于后面的交互部分识别该控件用的。
    <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title" android:textSize="20sp" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:src="@mipmap/sysu" app:layout_constraintBottom_toTopOf="@+id/search_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/title" />
    2.插入搜索输入框以及搜索按钮对于输入框要使用EditText控件,对于按钮要使用Button控件。对于输入框的显示内容,预先在string文件中写入,然后直接在控件中调用即可。对于button还用到了style属性,表示直接引用style写好的按钮样式。而style里面又调用了其他文件中已经预设好的属性,例如color中颜色。
    <style name="search_button"> <item name="android:textColor">@color/white</item> <item name="android:background">@drawable/button</item> </style>
    <EditText android:id="@+id/search_content" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="10dp" android:layout_marginTop="20dp" android:gravity="center" android:hint="@string/search_content" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/radioGroup" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/but1" app:layout_constraintTop_toBottomOf="@id/image" /> <Button android:id="@+id/but1" style="@style/search_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="20dp" android:layout_marginTop="20dp" android:text="@string/search_button" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/radioGroup" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/image" />
    3. 插入选择按钮选择按钮组要使用RadioGroup与RadioButton相配合,在group中设置边距以及大小,对于每一个radiobutton使用其他设置好的样式属性,在第一个选择按钮中设置checked属性设置为true就会默认第一个按钮被选定。
    <RadioGroup android:id="@+id/radioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:orientation="horizontal" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/search_content"> <RadioButton android:id="@+id/selection1" style="@style/MyRadioButton" android:layout_height="match_parent" android:checked="true" android:text="@string/selection1" /> <RadioButton android:id="@+id/selection2" style="@style/MyRadioButton" android:text="@string/selection2" /> <RadioButton android:id="@+id/selection3" style="@style/MyRadioButton" android:text="@string/selection3" /> <RadioButton android:id="@+id/selection4" style="@style/MyRadioButton" android:text="@string/selection4" /> </RadioGroup>
    这就基本完成了UI的界面设置,接下来要根据他们的id来设置一些函数实现实验要求,例如弹出对话框或者toast等等。
    4.获取搜索输入框的内容,以及点击搜索按钮显示提示这一步主要要调用findViewById这个函数来分别得到输入框以及按钮,给按钮设置监听函数setOnClickListener, 然后在里面对于输入框的内容searchContent.getText().toString()来进行判断,分别有三种情况,搜索内容为空,搜索内容为Health,搜索内容为其他。
    然后,关于对话框的显示要使用dialog,分别给它设置标题,中间内容以及按钮。而toast则要对于对话框的按钮来设置监听函数,当点击时候来Toast.makeText()显示一个具体的toast内容。
    Button button =(Button) findViewById(R.id.but1); final EditText searchContent = (EditText) findViewById(R.id.search_content); button.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view){ //搜索为空情况 if(TextUtils.isEmpty(searchContent.getText().toString())){ //弹出 Toast Toast.makeText(MainActivity.this, "搜索内容不能为空",Toast.LENGTH_SHORT).show(); } //搜索成功情况 else if(searchContent.getText().toString().equals("Health")){ AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("提示"); RadioButton temp = findViewById(radioGroup.getCheckedRadioButtonId()); dialog.setMessage(temp.getText().toString()+"搜索成功"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“确定”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“取消”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.show(); } //搜索失败情况 else{ AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("提示"); dialog.setMessage("搜索失败"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“确定”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“取消”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.show(); } } });
    4.对于选择按钮组的切换与上面相同,先要通过id来找到相应的控件,然后对于radioGroup来设置选择改变的监听函数,当切换的时候会根据选择的不同按钮上的信息来生成一个toast。
    final RadioGroup radioGroup = findViewById(R.id.radioGroup); final RadioButton radioButton = findViewById(radioGroup.getCheckedRadioButtonId()); radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener(){ @Override //选择变化时,弹出toast提示信息 public void onCheckedChanged(RadioGroup group, int checkedID){ String str = ""; RadioButton select1 = findViewById(R.id.selection1); RadioButton select2 = findViewById(R.id.selection2); RadioButton select3 = findViewById(R.id.selection3); RadioButton select4 = findViewById(R.id.selection4); if(select1.getId() == checkedID){ str = select1.getText().toString(); } else if(select2.getId() == checkedID){ str = select2.getText().toString(); } else if(select3.getId() == checkedID){ str = select3.getText().toString(); } else if(select4.getId() == checkedID){ str = select4.getText().toString(); } Toast.makeText(MainActivity.this, str + "被选中",Toast.LENGTH_SHORT).show(); } });
    3.1.3 实验遇到的困难以及解决思路1.关于UI部分的边距问题起初对于ConstraintLayout布局不熟悉,不理解为什么需要对于一个控件的左右边限制跟随另一个的左右边,单纯认为只需要改变margin即可完成布局。而实际情况时,根据布局出来的结果可以看到仅改变margin之后相对于父亲来改变距离,而不能完全地设置两个组件的相应距离。于是完成一个组件时候,对于下一个组件的上下左右边缘要根据相对应的组件来限制一下。
    而在修改UI的时候,多使用preview功能以及在xml下切换至design模式,可以清晰看出组件之间的边距关系,查看布局是否正确。

    2.如何让中间的搜索框以及搜索按钮以合适的大小安放在同一行?这个问题就是在ui部分一直困扰我的,由于搜索框与左边要有限制,在右边又要与搜索按钮有限制,而搜索框也要与右边有限制。这样设置 app:layout_constraintRight_toRightOf 等等需要十分注意。
    而且输入框的长度也要合适,当 android:layout_width=”wrap_parent” 时候仅显示了提示内容的长度。而 android:layout_width=”fill_parent” 时候又占满了整个显示屏,显然是不行。而选择固定长度则不符合我们安卓手机界面设计的原则,无法在各种机型中显示合理。
    经过查询多种资料,可以通过设置 android:layout_width=”0dp” 来使这个输入框自适应边距,因此问题迎刃而解。
    3.实现交互部分的api比较通用的找到控件的函数为findViewById,通过id来找到控件,这与我们设置的id就很关键了,必须要注意大小写以及名字的正确性。
    关于组件的监听函数,包括点击按钮,切换radiobutton等等,都要了解其中的参数,查看手册。
    3.2 Intent、Bundle的使用以及RecyclerView、ListView的应用3.2.1 实验截图下图为食物列表的展示,浮标图案为前往收藏夹:

    下图为收藏夹初始页面的展示,浮标图案为返回主页样式:

    下图为大豆食物的详情信息:

    下图为点击星星以及收藏按钮产生的事件截图:

    下图为收藏大豆事件后,收藏夹的信息截图:

    下图为长按大豆列表删除时的操作截图:

    下图为在食物列表长按食物删除的操作截图:

    3.2.2 实验步骤以及关键代码本次实验的内容有点多,要完成三个页面的设计以及不同活动之间的信息交互。
    1.完成从搜索页面跳转到FoodList页面由于上次的实验中完成了一个搜索的界面,我为了将两次实验连接到一起,因此在搜索页面搜索switch时候会跳转到食物列表页面(即本次实验内容)
    要记得在mainfest中注册该活动,否则会出现应用闪退的现象,下面的两个页面也是如此,不再详述。

    这里使用startActivity以及intent来实现页面的跳转。
    ...//切换至食物列表,第二周任务的衔接第一周任务else if(searchContent.getText().toString().equals("switch")){ Intent intent = new Intent(); intent.setClass(MainActivity.this, FoodList.class); startActivity(intent);}...
    2.存储食物数据为了保存这些食物数据,我新建了一个MyCollection类来存储,类函数包括构造函数以及各个参数的get,set函数,不必详述。
    public class MyCollection implements Serializable { private String name; //食物名字 private String content; //食物图标 private String type; //食物种类 private String material; //食物成分 private boolean is_collected; //是否被收藏 private boolean is_star; //是否被加星 public MyCollection(){ is_collected =false; } public MyCollection(String _name, String _content, String _type, String _material, boolean _is_star){ name = _name; content = _content; type =_type; material = _material; is_star = _is_star; is_collected = false; } ... //各种get,set函数
    3.利用RecycleView实现FoodList这一部分可以说是这次实验的难点,我用了一天的时间才能理解RecycleView的实现过程。一个RecycleView需要一个Adater以及一个Holder来实现,存储的数据利用Holder,而用户点击的事件则利用Adater.
    首先实现MyViewHolder类,它必须继承RecyclerView.ViewHolder。其中通过findViewById函数来查找列表的填充项,如果已经查找过了就从数组中直接拿出即可,这样可以加快应用的速度,优化性能。
    public class MyViewHolder extends RecyclerView.ViewHolder { private SparseArray<View> views; private View view; public MyViewHolder(View _view) { super(_view); view = _view; views = new SparseArray<View>(); } public <T extends View> T getView(int _viewId) { View _view = views.get(_viewId); if (_view == null) { //创建view _view = view.findViewById(_viewId); //将view存入views views.put(_viewId, _view); } return (T) _view; }}
    接着是MyRecyclerViewAdapter类,它必须继承RecyclerView.Adapter类,其中利用MyViewHolder来存储列表的数据。该类实现点击的功能,这里新建了item的点击监听器,包括单击以及长按两种操作。
    除此之外,它必须重构onCreateViewHolder,onBindViewHolder,getItemCount这三个函数。
    在onBindViewHolder中为item来重构点击事件,其中长按事件函数要返回false,不然会与单击事件同时触发
    public class MyRecyclerViewAdapter<T> extends RecyclerView.Adapter<MyViewHolder>{ private List<MyCollection> data; private Context context; private int layoutId; private OnItemClickListener onItemClickListener; public MyRecyclerViewAdapter(Context _context, int _layoutId, List<MyCollection> _data){ context = _context; layoutId = _layoutId; data = _data; } //点击事件的接口 public interface OnItemClickListener{ void onClick(int position); void onLongClick(int position); } public void setOnItemClickListener(OnItemClickListener _onItemClickListener) { this.onItemClickListener = _onItemClickListener; } //删除数据 public void deleteData(int position){ data.remove(position); } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item, parent, false)); return holder; } @Override public void onBindViewHolder(final MyViewHolder holder, int position) { //convert ((TextView)holder.getView(R.id.recipeName)).setText(data.get(position).getName()); ((TextView)holder.getView(R.id.img)).setText(data.get(position).getContent()); if (onItemClickListener != null) { //单击 holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onClick(holder.getAdapterPosition()); } }); //长按 holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { onItemClickListener.onLongClick(holder.getAdapterPosition()); return false; } }); } } @Override public int getItemCount() { if(!data.isEmpty()) return data.size(); return 0; }}
    我们先在FoodList.xml布局文件中预先设置了recycleview,以及新建一个item.xml来初始化列表项,包括两个textView组件来存放食物的标志以及文字。
    在FoodList.java中来通过recycleview的id来找到该组件,然后来通过adapter来设置。首先要利用setLatoutManager函数类似ListView来设置layout。
    然后设置监听器,单击跳转到详情页面根据点击的位置gotoDetail_for_Foodlist(position);该函数在后面部分叙述,此处只需知道它跳转到了详情页面。
    而长按时,要将数据删除,这里使用notifyItemRemoved(position);以及之前在Adapter实现了的删除函数来实现这一功能,最后弹出一个toast。
    RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(FoodList.this)); // 类似ListView final MyRecyclerViewAdapter myAdapter = new MyRecyclerViewAdapter<MyCollection>(FoodList.this, R.layout.item, data2); myAdapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() { @Override public void onClick(int position) { gotoDetail_for_Foodlist(position); } @Override public void onLongClick(int position) { myAdapter.notifyItemRemoved(position); myAdapter.deleteData(position); Toast.makeText(FoodList.this,"移除第"+(position+1)+"个商品", Toast.LENGTH_SHORT).show(); } }); recyclerView.setAdapter(myAdapter); //不使用动画情况,后面为其加自定义动画,见实验思考内容
    到这里,我就完成了第一个页面FoodList中列表的设计,但是还需要一个浮动按键。根据实验的教程来引入依赖后,在FoodList.xml为其新建组件,设定id。
    然后在食物列表页面通过id找到按键来处理,这里要求改变图片以及展示的内容,需要用到setVisibility,setImageResource这两个函数,通过一个tag来确定显示哪个页面,然后通过设置其是否展示或者展示哪张图片即可。
    //点击浮标事件 final FloatingActionButton f_but = findViewById(R.id.btn); f_but.setOnClickListener(new View.OnClickListener(){ boolean tag_for_foodlist = true; @Override public void onClick(View v){ if(tag_for_foodlist){ findViewById(R.id.recyclerView).setVisibility(View.GONE);//设置Foodlist不可见 findViewById(R.id.listView).setVisibility(View.VISIBLE); tag_for_foodlist = false; f_but.setImageResource(R.mipmap.mainpage); } else{ findViewById(R.id.recyclerView).setVisibility(View.VISIBLE); findViewById(R.id.listView).setVisibility(View.GONE);//设置Favourite不可见 tag_for_foodlist = true; f_but.setImageResource(R.mipmap.collect); } } });
    4.利用ListView实现Collection收藏夹页面首先在FoodList建立listview组件,然后才能通过id来找到。
    ListView就比前面简单很多,可以直接使用simpleAdapter来直接设置,只需调整传入的内容参数即可,点击的监听器要分别设单击事件,前往详情页面;长按事件,弹出询问框是否删除,这一部分是上一实验的内容不再详述。
    //ListView部分 ListView listview = (ListView) findViewById(R.id.listView); simpleAdapter = new SimpleAdapter(this, favourite, R.layout.item, new String[] {"img", "recipeName"}, new int[] {R.id.img, R.id.recipeName}); listview.setAdapter(simpleAdapter); listview.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { if(i != 0) gotoDetail_for_Collect(i); } }); listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) { // 处理长按事件 if(i != 0){ //弹出询问框 final int delete_num = i; AlertDialog.Builder dialog = new AlertDialog.Builder(FoodList.this); dialog.setTitle("删除"); dialog.setMessage("确定删除"+favourite.get(i).get("recipeName")+"?"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { favourite.remove(delete_num); simpleAdapter.notifyDataSetChanged(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }); dialog.show(); } return true; //这样长按与短按不会同时触发 } });
    5.利用Relative布局,Linear布局,Constraint布局实现Detail详情页面的UI部分这一部分需要我了解各种布局的一些具体情况,比如如何设置水平垂直居中,如何设置三分之一,如何与别的组件保持在一水平线上等等。
    因为上次实验已经使用了Constraint布局来设计UI,所以这里只分析一下对于用Relative布局的详情页面的顶部,要将顶部设置为三分之一,需要利用android:layout_weight=”1”这一属性,需要注意的是使用这一属性时,必须将高度设置为0,让其自动来匹配页面,以达成三分之一的效果。
    对于RelativeLayout布局,layout_alignParentLeft表示返回图标位于页面的左侧,其次食物名字要与返回图标的左侧对齐就要使用android:layout_alignLeft=”@id/back”,里面的参数为想要对齐的id。
    对于星星图标的处理可以预先设置为空星星,而且增加tag来为后面的变化做准备。
    而我为了保存星星的状态,不使用这一方法,所以在xml上不写图片,而在Detail.xml根据食物来动态生成。
    <RelativeLayout android:id="@+id/top" android:layout_width="match_parent" android:layout_height="0dp" android:background="#3F51BB" android:layout_weight="1" > <ImageView android:id="@+id/back" android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/back" android:layout_alignParentLeft="true" android:layout_marginTop="10dp" android:layout_marginStart="10dp" android:layout_marginLeft="10dp" /> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="20dp" android:layout_marginRight="20dp" android:text="牛奶" android:textSize="30dp" android:textColor="@color/white" android:layout_alignLeft="@id/back" android:layout_marginEnd="10dp" /> <ImageView android:id="@+id/star" android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentEnd="true" android:layout_alignTop="@+id/name" android:layout_marginEnd="20dp" android:layout_marginRight="20dp" android:layout_alignParentRight="true" /> </RelativeLayout>
    6.利用Intent,startActivity等实现不同活动之间的传递信息从前面点击监听器所绑定的跳转函数开始说明,这里是跳转到详情食物页面的函数,它必须根据坐标来将MyCollection中的内容读取出来,并将其放到bundle中,利用startActivityForResult来跳转到详情页面并等待返回参数来进行处理。这里需要的处理包括星星事件的点击,已经加入收藏夹的事件。
    private void gotoDetail_for_Foodlist(int position){ Intent intent = new Intent(); intent.setClass(FoodList.this,Details.class); Bundle bundle = new Bundle(); String s[] = new String [5]; s[0] = data2.get(position).getName(); s[1] = data2.get(position).getMaterial(); s[2] = data2.get(position).getType(); s[3] = data2.get(position).getContent(); s[4] = data2.get(position).getIs_star()?"yes":"no"; bundle.putStringArray("msg",s); intent.putExtras(bundle); startActivityForResult(intent,REQUEST_CODE);//REQUEST_CODE --> 1 }
    然后,在Detail.java中返回参数到主页面的函数。当点击星星以及收藏时候,我们只改变MyCollection的属性,而不是真正返回活动,而到点击返回按钮时候才根据这些改变的属性来传递不同的参数。

    当返回2时,表示详情页面出现了收藏事件,必须将MyCollection的信息传递回去bundle.putSerializable(“collect”, temp),并且使用setResult来返回参数
    当返回3时,表示详情页面出现了改变星星状态事件
    当返回4时,表示两种事件同时发生

    不然,则直接调用finish事件来结束活动。
    //处理返回按钮 final ImageView back_but = findViewById(R.id.back); back_but.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(temp.getIs_collected() == true && temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(4,intent); //RESULT_CODE --> 4 } //收藏夹 else if (temp.getIs_collected() == true) { Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2 } //保存星星状态 else if(temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(3,intent); //RESULT_CODE --> 3 } Details.this.finish(); } });
    这样在主页面中只需重构OnActivityResult函数即可以处理这些事件。处理结果为2时,从intent中拿回食物的信息,通知收藏夹列表来改变列表,这里使用我的私有函数refreshList,太过简单也不再细述,详情参见代码。
    而处理结果为3时,则要在两个列表中查找所有该食物的状态,更改星星的情况,以此实现星星状态的长期保存。
    // 为了获取结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == 2) { if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); refreshList(mc,simpleAdapter); } } else if(resultCode == 3){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } for(int i = 0; i < favourite.size(); i++){ if(favourite.get(i).get("recipeName").toString().equals(mc.getName())){ favourite.get(i).remove("star"); favourite.get(i).put("star",mc.getIs_star()); } } } } else if(resultCode == 4){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } refreshList(mc,simpleAdapter); } } }
    7.在Detail页面来根据不同的食物内容来动态生成UI界面这里的动态包括,详情页面上部的颜色,以及名字,营养成分等等。星星的状态也要动态生成。
    通过intent拿出的信息来改变xml中相对应id的组件,只需注意id的正确,以及颜色的选取即可。
    Bundle bundle=this.getIntent().getExtras(); str = bundle.getStringArray("msg"); TextView name = findViewById(R.id.name); name.setText(str[0]); TextView material = findViewById(R.id.material); material.setText("富含 "+ str[1]); TextView type = findViewById(R.id.type); type.setText(str[2]); temp = new MyCollection(str[0],str[3],str[2],str[1],false); //根据上次情况保存星星状态 final ImageView star_but = findViewById(R.id.star); if(str[4].equals("yes")){ star_but.setImageResource(R.mipmap.full_star); temp.setIs_star(true); } else{ star_but.setImageResource(R.mipmap.empty_star); temp.setIs_star(false); }
    3.2.3 实验遇到的困难以及解决思路1.RecycleView无法正确生成列表按照老师给的教程一步步写好Adapter与Holder后,运行应用时出现闪退情况。报错信息为,无法得到资源。一有报错,第一步当然是将报错信息扔上搜索引擎,但是网页上的信息都说是因为setText()里面的参数为String而不是其他。但细看自己的程序并没有出现setText的错误参数情况。
    然后,我对于类的传参开始找问题,结果发现是convert函数在传参的时候,没有找到资源,而是一个空的对象。于是再修改convert函数后,完成了这一部分的工作。
    2.收藏列表的错误点击收藏列表的第一项为“*”与“收藏夹”,这两个不应该被触发点击事件,否则会传递一个空的MyCollection到详情页面会出现报错。所以必须在点击收藏列表的监听函数时加一个判断,当点击的是第一个item时,不要触发跳转事件。
    3.食物详情页面的UI设计不符合位置这次详情页面的UI有点难度,对于三分之一的上部设置就弄了相当长的时间,当知道使用layout_weight时候,然而在实际使用的时候,却并没有达到三分之一的效果。后来,才知道没有将height设置为0dp,而是为wrap_content.。这样导致权重设置失败。
    其次,对于设置分割线以及收藏图标如何垂直居中,间距合适遇到了困难。由于我在下部使用的是ConstrainLayout布局,所以必须要以别的组件来作相对设置位置。这里我对于这两个组件,分别相对于parent的上方,以及下面分割线的下方作为限制。这样就好像上下两个作用力,使其位于垂直居中的位置。
    最后只需调整线条的长度以及图片的大小即可。
    <TextView android:id="@+id/ver_line" android:layout_width="2dp" android:layout_height="45dp" android:background="#1E000000" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/line1" android:gravity="center" android:layout_marginRight="10dp" app:layout_constraintRight_toLeftOf="@+id/collect" /> <ImageView android:id="@+id/collect" android:layout_width="45dp" android:layout_height="45dp" android:scaleType="fitXY" android:src="@mipmap/collect" app:layout_constraintBottom_toBottomOf="@id/line1" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_marginRight="20dp" />
    4.Detail页面与Foodlist页面进行交换信息时候,对于数据包的处理由于要使用intent来实现不同活动中的交互,必须将食物的信息传递到详情页面,以及在详情页面中改变后的食物信息传递回食物列表存储。于是,就要求他们交换信息时候必须要满足两个条件,第一是要一次传递一个食物对象,第二是要满足intent的信息交互函数。这里使用的是bundle的putSerializable函数,这也要求我们的食物类必须要实现Serializable类。
    Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2
    四、实验思考及感想4.1 UI界面设计与基础事件交互本次是第一次安卓开发的实验,主要关于UI界面的设计与一些简单的交互,这与我之前学过的web网页设计十分相似,定义组件以及通过id来对于每一个组件来设置一些监听函数,完成所需要的功能。
    但是,安卓开发上也有许多不同之处,对于java文件中必须要了解调用组件的监听函数,名字比较长,而且参数多,必须在平时熟练使用并要经常查阅手册。
    对于ui界面,我这次主要是通过xml的书写来生成界面,用里面的一些属性来定义组件的大小,边距等等,除此之外,安卓开发中还很讲究文件的分类,将string,color,style设置成另外的文件,在主的xml可以调用这些文件中的内容来实现,这样的好处便于修改界面的内容,例如可以根据这个来开发中文英文不同的ui界面版本。
    4.2 Intent、Bundle的使用以及RecyclerView、ListView的应用这次实验花了不少的时间来理解不同列表的实现方式,学习了不同ui布局的位置设置,活动之间的交互信息方法,按钮监听函数。
    但是,对于实验基本要求所做出了的应用程序还是有一些不太完美的地方,于是,我做了一些改进的地方(加分项),使其更加符合日常使用,包括对于详情列表星星的状态保存,在详情页面不按返回图标而是点击手机的返回键时无法收藏该食物状态,还为RecycleList加了一个自定义的动画效果,使其更加美观。
    4.2.1 对于星星状态的持久化改进星星的状态持久化,我实现出来的效果是当该食物被加星后,无论是在食物列表还是在收藏列表都会出现加星的同步状态,不会出现个别加星个别不加星。
    这里实现的持久化,实际就是给食物添加多一个is_star属性来判断该食物的状态,并将该状态传递到详情页面来动态处理。
    //根据上次情况保存星星状态 final ImageView star_but = findViewById(R.id.star); if(str[4].equals("yes")){ star_but.setImageResource(R.mipmap.full_star); temp.setIs_star(true); } else{ star_but.setImageResource(R.mipmap.empty_star); temp.setIs_star(false); }
    而在改变后返回到其他界面时,也要将改变了的星星状态返回,以此改变该食物在数据结构中的信息
    else if(resultCode == 3){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ //更新FoodList if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } for(int i = 0; i < favourite.size(); i++){ //更新CollectList if(favourite.get(i).get("recipeName").toString().equals(mc.getName())){ favourite.get(i).remove("star"); favourite.get(i).put("star",mc.getIs_star()); } } } }
    4.2.2 对于手机系统返回键的处理这里出现的bug是在详情页面点击收藏后,不按返回小图标,而是点击手机返回键时,无法收藏该食物。这是因为点击手机收藏键是没有将信息传递回主页面的,所以我们必须根据这个按键重构返回键的功能,来让该功能与点击返回小图标是一样的。
    当在详情页面,得到返回键被单击时,实现的功能与点击返回图标相同。而其他则继续执行系统的默认按键功能在最后添加return super.onKeyDown(keyCode, event);
    //点击返回时候,加入收藏也要生效 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { //两种情况同时实现 if(temp.getIs_collected() == true && temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(4,intent); //RESULT_CODE --> 4 } //收藏夹 else if (temp.getIs_collected() == true) { Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2 } //保存星星状态 else if(temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(3,intent); //RESULT_CODE --> 3 } else{ Details.this.finish(); } } return super.onKeyDown(keyCode, event); }
    4.2.3 实现RecycleList的动画在res文件夹建立anim文件夹来放置动画的xml文件,首先要建立layout_animation_fall_down.xml文件。
    其中animation为列表每一项item的动画,其文件在后面再实现,delay表示动画的延迟时间,animationOrder表示动画item的顺序是正常,即从大到小,在这里实现的效果就是从高到低。
    <?xml version="1.0" encoding="utf-8"?><layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:animation="@anim/item_animation_fall_down" android:delay="15%" android:animationOrder="normal" />
    接着对item实现layout_animation_fall_down.xml文件,来控制列表每一项的动画效果。
    translate组件中fromYDelta表示item首先位于y轴的上方20%出发,然后toYDelta表示item所要到达的位置,这里的0表示为回到本应该的位置。interpolator里面的属性表示减速实现动画过程。
    alpha组件表示透明度的变化,由0到1,加速实现动画过程。
    scale组件表示item的大小,由105%变化为100%,略微缩放动画。
    <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="500" > <translate android:fromYDelta="-20%" android:toYDelta="0" android:interpolator="@android:anim/decelerate_interpolator" /> <alpha android:fromAlpha="0" android:toAlpha="1" android:interpolator="@android:anim/decelerate_interpolator" /> <scale android:fromXScale="105%" android:fromYScale="105%" android:toXScale="100%" android:toYScale="100%" android:pivotX="50%" android:pivotY="50%" android:interpolator="@android:anim/decelerate_interpolator" /></set>
    这样就实现了一个列表的从上到下,逐渐出现的动画。
    4.3 感想通过不断的学习,总算理解了android的一些机制,也能简单的写出了一个程序了。但是对于java语言的虚函数,静态函数,接口,数据类型等等都需要加强,这会使我更方便地理解类与类之间的关系。对于ui的设计要熟练掌握三种布局的运用,可以适当给某些组件先赋值通过preview来查看位置,再在java文件中实现动态赋值,这样做既能保证ui也能动态生成页面。这次实验使用的是绑定数据是运用数组,猜想未来应该可以引入数据库的绑定,这样会使代码更加简洁。
    1  留言 2019-07-11 12:12:51
  • 编程实现U盘插入自动复制U盘内容到本地

    背景U盘插入计算机后,不用任何操作,程序自动将U盘里的文件都拷贝到本地计算机上。这个功能是我自己开发的“恶魔的结界”系列程序里的一个小功能,至于有什么用,那就看个人的爱好了。在此,只探讨技术,不探讨用途。
    现在,我就对它进行解析,整理成文档,分享给大家。
    实现原理这个程序的实现,可以分成两个部分:

    U盘设备插入的监控,获取U盘盘符
    根据U盘盘符,遍历U盘文件,并进行复制操作

    首先,对U盘设备插入的监控,可以参考我写的 “编程实现监控U盘或者其它移动设备的插入和拔出” 这篇文章,使用方法是对程序添加 WM_DEVICECHANGE 消息处理函数,并根据 DEV_BROADCAST_VOLUME 结构体的 dbcv_unitmask 逻辑单元掩码来计算出插入设备U盘的盘符。
    我们成功获取了U盘盘复制后,也就知道了U盘的路径了。所以,我们使用WIN 32 API 函数 FindFirstFile 和 FindNextFile 从U盘的根目录进行文件遍历,具体的遍历方法解析可以参考本站上其他人写的 “使用FindFirstFile和FindNextFile函数实现文件搜索遍历” 这篇文章。 对于遍历到的文件,我们就调用 CopyFile 函数将它静默拷贝到本地指定的存储路径中。
    这样,经过上述的两步操作,我们就可以实现插入U盘,自动拷贝U盘文件到本地的功能了。
    编码实现U盘插入监控// 监控U盘插入并获取U盘盘符LRESULT CUDiskCopy_TestDlg::OnDeviceChange(WPARAM wParam, LPARAM lParam){ switch (wParam) { // 设备已经插入 case DBT_DEVICEARRIVAL: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符, 开始执行拷贝, 从目标设备拷贝到本地上 SearchFile(szDriver); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } default: break; } return 0;}
    U盘文件遍历及拷贝// 遍历文件并复制void SearchFile(char *pszDirectory){ // 搜索指定类型文件 DWORD dwBufferSize = 2048; char *pszFileName = NULL; char *pTempSrc = NULL; WIN32_FIND_DATA FileData = { 0 }; BOOL bRet = FALSE; // 申请动态内存 pszFileName = new char[dwBufferSize]; pTempSrc = new char[dwBufferSize]; // 构造搜索文件类型字符串, *.*表示搜索所有文件类型 ::wsprintf(pszFileName, "%s\\*.*", pszDirectory); // 搜索第一个文件 HANDLE hFile = ::FindFirstFile(pszFileName, &FileData); if (INVALID_HANDLE_VALUE != hFile) { do { // 要过滤掉 当前目录"." 和 上一层目录"..", 否则会不断进入死循环遍历 if ('.' == FileData.cFileName[0]) { continue; } // 拼接文件路径 ::wsprintf(pTempSrc, "%s\\%s", pszDirectory, FileData.cFileName); // 判断是否是目录还是文件 if (FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // 目录, 则继续往下递归遍历文件 SearchFile(pTempSrc); } else { // 文件, 执行复制操作, 复制到本地上 char szNewFile[MAX_PATH] = "C:\\Users\\Desktop\\test\\"; ::lstrcat(szNewFile, FileData.cFileName); ::CopyFile(pTempSrc, szNewFile, FALSE); } // 搜索下一个文件 } while (::FindNextFile(hFile, &FileData)); } // 关闭文件句柄 ::FindClose(hFile); // 释放内存 delete[]pTempSrc; pTempSrc = NULL; delete[]pszFileName; pszFileName = NULL;}
    程序测试我们运行程序后,插入U盘,然后等待一会儿后,我们打开本地上保存U盘拷贝数据的目录,发现成功拷贝U盘里的文件。

    总结为了防止程序在秘密拷贝U盘数据的时候,程序会卡死,所以,可以创建一个多线程,把拷贝文件的操作放到多线程里去执行,这样就不会阻塞主线程了。
    同时,创建本文演示的这个程序还没有对程序的窗口进行隐藏,如果你想要把这个程序做得比较隐蔽的话,可以参考本站上其他人写的 “编程实现MFC程序窗口一运行立马隐藏” 这篇文档,里面有介绍如何一开就隐藏窗口程序。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-12-05 21:21:26
  • 编程实现监控U盘或者其它移动设备的插入和拔出

    背景如果在没有阅读本文之前,可能你会认为编程实现监控U盘或者其它移动设备的插入和拔出,是一个很难的事情,或者是一个很靠近系统底层的事情。其实,这些你完全不用担心,Windows 已经为我们都设计好了。
    我们都知道,Windows应用程序都是消息(事件)驱动的,任何一个窗口都能够接收消息,并对该消息做出相应的处理。同样,U盘或者其它移动设备的插入或者拔出也会有相应的消息与之对应,这个消息便是 WM_DEVICECHANGE。顾名思义,这个消息就是设备更改的时候产生的。那么,我们的程序同样可以捕获到这个消息,只要我们对这个消息做出处理就可以了。
    现在,我就把这个程序实现的过程整理成文档,分享给大家。
    函数介绍WM_DEVICECHANGE 消息
    通知应用程序对设备或计算机的硬件配置进行更改。窗口通过其WindowProc函数接收此消息。
    LRESULT CALLBACK WindowProc( HWND hwnd, // handle to window UINT uMsg, // WM_DEVICECHANGE WPARAM wParam, // device-change event LPARAM lParam // event-specific data );
    参数

    hwnd:窗口的句柄。
    uMsg:WM_DEVICECHANGE标识符。
    wParam:发生的事件。 该参数可以是Dbt.h头文件中的以下值之一:




    VALUE
    MEANING




    DBT_CONFIGCHANGECANCELED
    更改当前配置(停靠或停靠)的请求已被取消


    DBT_CONFIGCHANGED
    由于停靠或停靠,当前配置已更改


    DBT_CUSTOMEVENT
    发生了自定义事件


    DBT_DEVICEARRIVAL
    已插入设备或介质,现在可以使用


    DBT_DEVICEQUERYREMOVE
    请求删除设备或介质的权限。 任何应用程序可以拒绝此请求并取消删除


    DBT_DEVICEQUERYREMOVEFAILED
    删除设备或介质的请求已被取消


    DBT_DEVICEREMOVECOMPLETE
    已移除设备或介质片


    DBT_DEVICEREMOVEPENDING
    一个设备或一块介质即将被删除。 不能否认


    DBT_DEVICETYPESPECIFIC
    发生设备特定事件


    DBT_DEVNODES_CHANGED
    已将设备添加到系统中或从系统中删除


    DBT_QUERYCHANGECONFIG
    请求权限更改当前配置(停靠或停靠)


    DBT_USERDEFINED
    此消息的含义是用户定义的




    lParam:指向包含事件特定数据的结构的指针。 其格式取决于wParam参数的值。 有关详细信息,请参阅每个事件的文档。
    返回值

    返回TRUE以授予请求。返回BROADCAST_QUERY_DENY以拒绝该请求。

    DEV_BROADCAST_HDR 结构体
    typedef struct _DEV_BROADCAST_HDR { DWORD dbch_size; DWORD dbch_devicetype; DWORD dbch_reserved;} DEV_BROADCAST_HDR, *PDEV_BROADCAST_HDR;
    成员

    dbch_size这个结构的大小,以字节为单位。如果这是用户定义的事件,则该成员必须是此标头的大小,加上_DEV_BROADCAST_USERDEFINED结构中的可变长度数据的大小。
    dbch_devicetype设备类型,用于确定前三个成员之后的事件特定信息。 该成员可以是以下值之一:




    VALUE
    MEANING




    DBT_DEVTYP_DEVICEINTERFACE
    设备类。 此结构是DEV_BROADCAST_DEVICEINTERFACE结构


    DBT_DEVTYP_HANDLE
    文件系统句柄。 这个结构是一个DEV_BROADCAST_HANDLE结构


    DBT_DEVTYP_OEM
    OEM或IHV定义的设备类型。 该结构是DEV_BROADCAST_OEM结构


    DBT_DEVTYP_PORT
    端口设备(串行或并行)。 这个结构是一个DEV_BROADCAST_PORT结构


    DBT_DEVTYP_VOLUME
    逻辑卷。 这个结构是一个DEV_BROADCAST_VOLUME结构




    dbch_reserved
    保留。


    DEV_BROADCAST_VOLUME 结构体
    typedef struct _DEV_BROADCAST_VOLUME { DWORD dbcv_size; DWORD dbcv_devicetype; DWORD dbcv_reserved; DWORD dbcv_unitmask; WORD dbcv_flags;} DEV_BROADCAST_VOLUME, *PDEV_BROADCAST_VOLUME;
    成员

    dbcv_size这个结构的大小,以字节为单位。
    dbcv_devicetype设置为DBT_DEVTYP_VOLUME(2)。
    dbcv_reserved保留; 不使用。
    dbcv_unitmask标识一个或多个逻辑单元的逻辑单元掩码。 掩码中的每个位对应于一个逻辑驱动器。 位0表示驱动器A,位1表示驱动器B,依此类推。
    dbcv_flags此参数可以是以下值之一:




    VALUE
    MEANING




    DBTF_MEDIA
    更改影响驱动器中的介质。 如果未设置,更改将影响物理设备或驱动器


    DBTF_NET
    指示逻辑卷是一个网络卷




    实现原理由于我们主要是对设备的插入和拔出做操作,所以,只需要对消息回调函数的参数 wParam 进行判断,是否为设备已插入操作 DBT_DEVICEARRIVAL 和 设备已移除操作 DBT_DEVICEREMOVECOMPLETE。然后再重点分析相应操作对应的 lParam 参数里存储的信息数据,从而分析出产生操作设备的盘符。
    设备已插入 DBT_DEVICEARRIVAL首先,当 wParam 为设备已插入操作 DBT_DEVICEARRIVAL的时候,我们就可以知道是有设备已经插入了。
    接下来就是要获取设备的盘符,这需要对参数 lParam 进行分析,此时 lParam 则表示指向 DEV_BROADCAST_HDR 结构的指针。由上述的结构体介绍中,我们可以知道,要获取盘符,就首先要判断 DEV_BROADCAST_HDR 结构体中的设备类型 dbch_devicetype 是否为逻辑卷 DBT_DEVTYP_VOLUME。因为其它的消息类型,是不会产生盘符的。只有消息类型为 DBT_DEVTYP_VOLUME 逻辑卷,才会产生盘符。
    由上述结构体介绍中知道,当消息类型为 DBT_DEVTYP_VOLUME 逻辑卷的时候, 参数 lParam 实际上是结构体 DEV_BROADCAST_VOLUME。其中,结构体 DEV_BROADCAST_VOLUME 的 dbcv_unitmask 成员标识一个或多个逻辑单元的逻辑单元掩码,掩码中的每个位对应于一个逻辑驱动器。 位0表示驱动器A,位1表示驱动器B,依此类推。所以,我们可以根据 dbcv_unitmask 计算出设备生成的盘符。
    设备已移除 DBT_DEVICEREMOVECOMPLETE首先,当 wParam 为设备已移除操作 DBT_DEVICEREMOVECOMPLETE 的时候,我们就可以知道是有设备已经移除了。
    接下来就是要获取移除设备原来的盘符,这需要对参数 lParam 进行分析,此时 lParam 则表示指向 DEV_BROADCAST_HDR 结构的指针。接下来的分析,和上面设备插入时,获取设备盘符的分析是一样的,在此就不重复了。
    编程实现给程序添加 WM_DEVICECHANGE 的消息响应,并声明定义一个处理函数,处理相应的消息。
    对于 Windows应用程序 来说,只需要在窗口消息处理函数中,增加消息类型WM_DEVICECHANGE 的判断即可。然后,调用处理函数进程处理。
    对于 MFC 程序来说,则需要自定义 WM_DEVICECHANGE 消息响应函数。在主对话框类的头文件中声明处理函数 :
    LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam);
    然后,再在主对话框类的消息映射列表中,添加 WM_DEVICECHANGE 与消息处理函数的映射:
    BEGIN_MESSAGE_MAP(CWM_DEVICECHANGE_MFC_TestDlg, CDialogEx) … …(省略) ON_MESSAGE(WM_DEVICECHANGE, OnDeviceChange) … …(省略)END_MESSAGE_MAP()
    那么,Windows应用程序 和 MFC 程序对 WM_DEVICECHANGE 消息的消息处理函数定义都是相同的:
    LRESULT OnDeviceChange(WPARAM wParam, LPARAM lParam){ switch (wParam) { // 设备已经插入 case DBT_DEVICEARRIVAL: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:\\"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符 ::MessageBox(NULL, szDriver, "设备已插入", MB_OK); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } // 设备已经移除 case DBT_DEVICEREMOVECOMPLETE: { PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; // 逻辑卷 if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype) { // 根据 dbcv_unitmask 计算出设备盘符 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb; DWORD dwDriverMask = lpdbv->dbcv_unitmask; DWORD dwTemp = 1; char szDriver[4] = "A:\\"; for (szDriver[0] = 'A'; szDriver[0] <= 'Z'; szDriver[0]++) { if (0 < (dwTemp & dwDriverMask)) { // 获取设备盘符 ::MessageBox(NULL, szDriver, "设备已移除", MB_OK); } // 左移1位, 接着判断下一个盘符 dwTemp = (dwTemp << 1); } } break; } default: break; } return 0;}
    程序测试这是,我们直接运行程序,插入U盘,程序成功弹窗提示有U盘插入,并给出U盘的盘符。

    我们把U盘拔出,程序成功弹窗提示有U盘拔出,并给出U盘的盘符。

    总结本文给出了 MFC程序 和 Windows应用程序 的例子,实现监控U盘或其它移动设备的插入和拔出。其中,我们需要注意理解 DEV_BROADCAST_VOLUME 的 dbcv_unitmask 逻辑单元掩码。它是 4字节 32位,它的每一位都对应一个盘符,从 A 开始计数。如果位中的数值为1,则表示设备操作产生的盘符,为 0,则表示没有产生盘符。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-28 20:55:37
  • 使用VS2013实现修改程序的资源

    背景之前写了一个从程序中释放自定义资源功能的小程序,既然资源可以被定位出来,并能够获取其数据写入到本地文件中。那么,自然可以相信,我们能够实现一个这样的小程序,替换或者说更改其他程序中的资源。
    事实上,的确是可以这么做的。因为EXE格式的文件是PE文件,如果熟悉PE文件格式,那么就会了解PE文件中有个资源节表块,按一定的结构格式专门存放着程序的所有资源。所以,我们可以根据PE结构去获取程序的资源。
    当然,如果你不了解PE结构的话,也能实现这个功能的小程序。因为,Windows为我们提供了相应操作资源的API函数接口,我们可以不用去了解API函数具体的实现原理,具体是如何遍历或是更改PE结构的,就能实现我们想要的功能。
    现在,我把这个小程序的实现原理和实现过程,写成文档,分享给大家。
    函数介绍BeginUpdateResource 介绍
    该函数返回一个可被UpdateResource函数使用的句柄以便在一个可执行文件中增加、删除或替换资源。
    函数声明
    HANDLE BeginUpdateResource( LPCTSTR pFileName, // executable file name BOOL bDeleteExistingResources // deletion option );
    参数

    pFileName:指向一个表示结束的空字符串指针,它是用来指定用以更新资源的基于32-位可执行文件的文件名。应用程序必须获得访问这个文件的可写权限,并且此文件在当前状态下不能被执行。如果pFileName未被指定完全路径,系统将在当前路径下搜寻此文件。bDeleteExistingResources:说明是否删除pFileName参数指定的现有资源。如果这个参数为TRUE则现有的资源将被删除,而更新可执行文件只包括由UpdateResource函数增加的资源。如果这个参数为FALSE,则更新的可执行文件包括现有的全部资源,除非通过UpdateResource特别说明被删除或是替换的。
    返回值

    如果此函数运行成功,其值将通过使用UpdateResource和EndUpdateResource函数返回一个句柄。如果被指定的文件不是一个可执行文件,或者可执行文件已被装载,或者文件不存在,或是文件不能被打开写入时,则返回值为空。若想获得更多的错误信息,请调用GetLastError函数。

    UpdateResource 介绍
    增加 删除 或替文件中的资源。
    函数声明
    BOOL UpdateResource( HANDLE hUpdate, // update-file handle LPCTSTR lpType, // resource type LPCTSTR lpName, // resource name WORD wLanguage, // language identifier LPVOID lpData, // resource data DWORD cbData // length of resource data );
    参数

    hUpdate:指定更新文件句柄。此句柄由BeginUpdateResource函数返回。lpType:指向说明将被更新的资源类型的字符串,它以NULL为终止符。这个参数可以是一个通过宏MAKENTRESOURCE传递的整数值,含义参见EnumResLangProc\lpType。lpName:指向说明待被更新的资源名称的字符串,它以NULL为终止符。这个参数可以是一个通过宏MAKEINTRESOURCE传递的整数值。wLanguage:指定将被更新资源的语言标识。要了解基本的语言标识符以及由这些标识符组成的字语言标识符的列表,可参见宏MAKELANGID。lpData:指向被插入可执行文件的资源数据的指针。如果资源是预定义类型值之一,那么数据必须是有效且适当排列的。注意这是存储在可执行文件中原始的一进制数据,而不是由Loadlcon,LoadString或其他装载特殊资源函数提供的数据。所有包含字符串、文本的数据必须是Unicode格式;IpData不能指向ANSI数据。如果lpData为NULL,所指定的资源将从可执行文件中被删除。cbData:指定lpData中的资源数据数据大小,以字节计数。
    返回值

    如果函数运行成功,返回值为非零;如果函数运行失败,返回值为零。若想获得更多的错误信息,请调用GetLastError函数。

    EndUpdateResource 介绍
    终止在可执行文件中的资源更新。
    函数声明
    BOOL EndUpdateResource( HANDLE hUpdate, // update-file handle BOOL fDiscard // write option );
    参数

    hUpdate:用于资源更新的句柄。此句柄通过BeginUpdateResource函数返回。fDiscard:用来说明是否向可执行文件中写入资源更新内容。如果此参数为TRUE,则在可执行文件中无变化;如果此参数为FALSE,则在可执行文件中写入变化。
    返回值

    如果函数运行成功,并且通过调用UpdateResource函数指定的不断积聚的资源修正内容被写入指定的可执行文件,那么其返回值为非零。如果函数运行失败,其返回值为零。若想获得更多的错误信息,请调用GetLastError函数。

    实现原理现在,我们结合上述介绍的API函数,讲解下程序实现的原理。

    首先,我们先打开替换文件,并获取替换文件的数据
    然后,我们对目标文件的资源进行替换。先定根据目标文件的“资源类型”以及“资源名称”定位出被替换的资源,接着将替换文件的资源数据写入进去
    结束资源的更改操作

    这样,我们就完成了资源的更改操作。
    编码实现BOOL ChangeExeRes(char *pszSrcFileName, char *pszInstallFileName, UINT uiDestResId, char *pszDestResType){ BYTE *pData = NULL; DWORD dwDataSize = 0; // 打开目标文件获取数据 HANDLE hFile = ::CreateFile(pszInstallFileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { FreeRes_ShowError("CreateFile"); return FALSE; } // 获取文件大小 dwDataSize = ::GetFileSize(hFile, NULL); // 申请动态内存 pData = new BYTE[dwDataSize]; if (NULL == pData) { FreeRes_ShowError("new"); return FALSE; } // 读取数据 DWORD dwRet = 0; if (FALSE == ::ReadFile(hFile, pData, dwDataSize, &dwRet, NULL)) { FreeRes_ShowError("ReadFile"); return FALSE; } // 关闭文件句柄 ::CloseHandle(hFile); // 更改资源 HANDLE hUpdate = ::BeginUpdateResource(pszSrcFileName, FALSE); if (NULL == hUpdate) { FreeRes_ShowError("BeginUpdateResource"); return FALSE; } // 如果资源ID存在, 则替换资源; 否则创建资源 BOOL bRet = ::UpdateResource(hUpdate, pszDestResType, (LPCSTR)uiDestResId, LANG_NEUTRAL, pData, dwDataSize); if (FALSE == bRet) { FreeRes_ShowError("UpdateResource"); return FALSE; } ::EndUpdateResource(hUpdate, FALSE); // 释放内存 delete[]pData; pData = NULL; return TRUE;}
    程序测试我们在 main 函数中调用上述封装好的接口函数,进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 更改资源 BOOL bRet = ChangeExeRes("C:\\Users\\DemonGan\\Desktop\\ResourceFree_Test\\Debug\\ResourceFree_Test.exe", "C:\\Users\\DemonGan\\Desktop\\test.txt", 130, "MYRES"); if (FALSE == bRet) { printf("Change Resource Error!\n"); } else { printf("Change Resource OK!\n"); } system("pause"); return 0;}
    测试结果:
    在测试之前,使用资源编辑器 “eXeScope” 查看 “ResourceFree_Test.exe”文件里的资源情况:资源类型“MYRES”,资源ID为“130”的资源内容如下图所示。

    然后,我们创建一个替换上述资源的替换文件 “test.txt”,内容如下图所示。

    然后,运行我们的更改资源的小程序,提示更改资源成功。

    然后,使用再使用资源编辑器 “eXeScope” 查看 “ResourceFree_Test.exe”文件里的资源情况:资源类型“MYRES”,资源ID为“130”的资源内容如下图所示。

    由上图可见,资源已经被成功更改替换了,替换的数据内容正是我们的替换文件的数据内容。所以,程序测试成功。
    总结对于这个小程序来说,只需理解使用BeginUpdateResource、UpdateResource以及EndUpdateResource这 3 个API函数就好。然后,自己动手多操作一两次,加深理解。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-16 16:37:41

发送私信

如果这世界上真有奇迹,那只是努力的另一个名字

12
文章数
9
评论数
eject