扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
原理:自定义布局的onSizeChanged()方法,在其中增加一个监听接口,当软键盘显示或隐藏使得布局尺寸发生改变,就能捕获到这个事件。
公司主营业务:做网站、成都网站建设、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。成都创新互联公司是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。成都创新互联公司推出新乐免费做网站回馈大家。
具体方法如下:
首先,在清单文件Manifest.xml中的对应的Activity标签内设置
android:windowSoftInputMode=”adjustResize”
作用是当软键盘显示或隐藏时,该Activity主窗口总是会被调整大小以便留出软键盘的空间。唯有这样才能保证布局触发onSizeChanged()方法。
然后,自定义一个布局,具体是RelativeLayout、LinearLayout或是其它的,根据实际情况而定,自定义的目的是在其onSizeChanged()方法中增加一个监听接口。这里给出一个自定义RelativeLayout布局代码:
public class CustomRelativeLayout extends RelativeLayout {
private OnSizeChangedListener listener;
public CustomRelativeLayout(Context context) {
super(context);
}
public CustomRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.d(TAG.CUSTOM_VIEW, “onSizeChanged”);
super.onSizeChanged(w, h, oldw, oldh);
if (listener != null) {
listener.onSizeChanged(w, h, oldw, oldh);
}
}
public void setOnSizeChangedListener(OnSizeChangedListener listener) {
this.listener = listener;
}
/** * Activity主窗口大小改变时的回调接口(本示例中,等价于软键盘显示隐藏时的回调接口) */
public interface OnSizeChangedListener {
public void onSizeChanged(int w, int h, int oldw, int oldh);
}
}
最后,在程序中使用此接口(xxx.setOnSizeChangedListener(…))即可实现监听键盘隐藏或显示事件。
Android是一个针对触摸屏专门设计的操作系统,当点击编辑框,系统自动为用户弹出软键盘,以便用户进行输入。
那么,弹出软键盘后必然会造成原有布局高度的减少,那么系统应该如何来处理布局的减少?我们能否在应用程序中进行自定义的控制?这些是本文要讨论的重点。
一、软键盘显示的原理
软件盘的本质是什么?软键盘其实是一个Dialog!
InputMethodService为我们的输入法创建了一个Dialog,并且将该Dialog的Window的某些参数(如Gravity)进行了设置,使之能够在底部或者全屏显示。当我们点击输入框时,系统对活动主窗口进行调整,从而为输入法腾出相应的空间,然后将该Dialog显示在底部,或者全屏显示。
二、活动主窗口调整
android定义了一个属性,名字为windowSoftInputMode, 用它可以让程序可以控制活动主窗口调整的方式。我们可以在AndroidManifet.xml中对Activity进行设置。如:android:windowSoftInputMode="stateUnchanged|adjustPan"
该属性可选的值有两部分,一部分为软键盘的状态控制,另一部分是活动主窗口的调整。前一部分本文不做讨论,请读者自行查阅android文档。
模式一,压缩模式
windowSoftInputMode的值如果设置为adjustResize,那么该Activity主窗口总是被调整大小以便留出软键盘的空间。
我们通过一段代码来测试一下,当我们设置了该属性后,弹出输入法时,系统做了什么。
重写Layout布局:
1. public class ResizeLayout extends LinearLayout{
2. private static int count = 0;
3.
4. public ResizeLayout(Context context, AttributeSet attrs) {
5. super(context, attrs);
6. }
7.
8. @Override
9. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
10. super.onSizeChanged(w, h, oldw, oldh);
11.
12. Log.e("onSizeChanged " + count++, "=onResize called! w="+w + ",h="+h+",oldw="+oldw+",oldh="+oldh);
13. }
14.
15. @Override
16. protected void onLayout(boolean changed, int l, int t, int r, int b) {
17. super.onLayout(changed, l, t, r, b);
18. Log.e("onLayout " + count++, "=OnLayout called! l=" + l + ", t=" + t + ",r=" + r + ",b="+b);
19. }
20.
21. @Override
22. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
23. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
24.
25. Log.e("onMeasure " + count++, "=onMeasure called! widthMeasureSpec=" + widthMeasureSpec + ", heightMeasureSpec=" + heightMeasureSpec);
26. }
我们的布局设置为:
1. com.winuxxan.inputMethodTest.ResizeLayout
2. xmlns:android=""
3. android:id="@+id/root_layout"
4. android:layout_width="fill_parent"
5. android:layout_height="fill_parent"
6. android:orientation="vertical"
7.
8.
9. EditText
10. android:layout_width="fill_parent"
11. android:layout_height="wrap_content"
12. /
13.
14. LinearLayout
15. android:id="@+id/bottom_layout"
16. android:layout_width="fill_parent"
17. android:layout_height="fill_parent"
18. android:orientation="vertical"
19. android:gravity="bottom"s
20.
21. TextView
22. android:layout_width="fill_parent"
23. android:layout_height="wrap_content"
24. android:text="@string/hello"
25. android:background="#77777777"
26. /
27. /LinearLayout
28. /com.winuxxan.inputMethodTest.ResizeLayout
AndroidManifest.xml的Activity设置属性:android:windowSoftInputMode = "adjustResize"
运行程序,点击文本框,查看调试信息:
E/onMeasure 6(7960): =onMeasure called! widthMeasureSpec=1073742144, heightMeasureSpec = 1073742024
E/onMeasure 7(7960): =onMeasure called! widthMeasureSpec=1073742144, heightMeasureSpec = 1073742025
E/onSizeChanged 8(7960): =onSizeChanged called! w=320,h=201,oldw=320,oldh=377
E/onLayout 9(7960): =OnLayout called! l=0, t=0,r=320,b=201
从调试结果我们可以看出,当我们点击文本框后,根布局调用了onMeasure,onSizeChanged和onLayout。
实际上,当设置为adjustResize后,软键盘弹出时,要对主窗口布局重新进行measure和layout,而在layout时,发现窗口的大小发生的变化,因此调用了onSizeChanged。
从下图的运行结果我们也可以看出,原本在下方的TextView被顶到了输入法的上方。
模式二,平移模式
windowSoftInputMode的值如果设置为adjustPan,那么该Activity主窗口并不调整屏幕的大小以便留出软键盘的空间。相反,当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分。这个通常是不期望比调整大小,因为用户可能关闭软键盘以便获得与被覆盖内容的交互操作。
上面的例子中,我们将AndroidManifest.xml的属性进行更改:android: windowSoftInputMode = "adjustPan"
重新运行,并点击文本框,查看调试信息:
E/onMeasure 6(8378): =onMeasure called! widthMeasureSpec=1073742144, heightMeasureSpec=1073742200
E/onMeasure 7(8378): =onMeasure called! widthMeasureSpec=1073742144, heightMeasureSpec=1073742201
E/onLayout 8(8378): =OnLayout called! l=0, t=0,r=320,b=377
我们看到:系统也重新进行了measrue和layout,但是我们发现,layout过程中onSizeChanged并没有调用,这说明输入法弹出前后并没有改变原有布局的大小。
从下图的运行结果我们可以看到,下方的TextView并没有被顶到输入法上方。
事实上,当输入框不会被遮挡时,该模式没有对布局进行调整,然而当输入框将要被遮挡时,窗口就会进行平移。也就是说,该模式始终是保持输入框为可见。如下图,整个窗口,包括标题栏均被上移,以保证文本框可见。
模式三 自动模式
当属性windowSoftInputMode被设置为adjustUspecified时,它不被指定是否该Activity主窗口调整大小以便留出软键盘的空间,或是否窗口上的内容得到屏幕上当前的焦点是可见的。系统将自动选择这些模式中一种主要依赖于是否窗口的内容有任何布局视图能够滚动他们的内容。如果有这样的一个视图,这个窗口将调整大小,这样的假设可以使滚动窗口的内容在一个较小的区域中可见的。这个是主窗口默认的行为设置。
也就是说,系统自动决定是采用平移模式还是压缩模式,决定因素在于内容是否可以滚动。
三、侦听软键盘的显示隐藏
有时候,借助系统本身的机制来实现主窗口的调整并非我们想要的结果,我们可能希望在软键盘显示隐藏的时候,手动的对布局进行修改,以便使软键盘弹出时更加美观。这时就需要对软键盘的显示隐藏进行侦听。
直接对软键盘的显示隐藏侦听的方法本人没有找到,如果哪位找到的方法请务必告诉本人一声。还有本方法针对压缩模式,平移模式不一定有效。
我们可以借助软键盘显示和隐藏时,对主窗口进行了重新布局这个特性来进行侦听。如果我们设置的模式为压缩模式,那么我们可以对布局的onSizeChanged函数进行跟踪,如果为平移模式,那么该函数可能不会被调用。
我们可以重写根布局,因为根布局的高度一般情况下是不发生变化的。
假设跟布局为线性布局,模式为压缩模式,我们写一个例子,当输入法弹出时隐藏某个view,输入法隐藏时显示某个view。
1. public class ResizeLayout extends LinearLayout{
2. private OnResizeListener mListener;
3.
4. public interface OnResizeListener {
5. void OnResize(int w, int h, int oldw, int oldh);
6. }
7.
8. public void setOnResizeListener(OnResizeListener l) {
9. mListener = l;
10. }
11.
12. public ResizeLayout(Context context, AttributeSet attrs) {
13. super(context, attrs);
14. }
15.
16. @Override
17. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
18. super.onSizeChanged(w, h, oldw, oldh);
19.
20. if (mListener != null) {
21. mListener.OnResize(w, h, oldw, oldh);
22. }
23. }
24. }
在我们的Activity中,通过如下方法调用:
1. public class InputMethodTestActivity extends Activity {
2. private static final int BIGGER = 1;
3. private static final int SMALLER = 2;
4. private static final int MSG_RESIZE = 1;
5.
6. private static final int HEIGHT_THREADHOLD = 30;
7.
8. class InputHandler extends Handler {
9. @Override
10. public void handleMessage(Message msg) {
11. switch (msg.what) {
12. case MSG_RESIZE: {
13. if (msg.arg1 == BIGGER) {
14. findViewById(R.id.bottom_layout).setVisibility(View.VISIBLE);
15. } else {
16. findViewById(R.id.bottom_layout).setVisibility(View.GONE);
17. }
18. }
19. break;
20.
21. default:
22. break;
23. }
24. super.handleMessage(msg);
25. }
26. }
27.
28. private InputHandler mHandler = new InputHandler();
29.
30. /** Called when the activity is first created. */
31. @Override
32. public void onCreate(Bundle savedInstanceState) {
33. super.onCreate(savedInstanceState);
34. setContentView(R.layout.main);
35.
36. ResizeLayout layout = (ResizeLayout) findViewById(R.id.root_layout);
37. layout.setOnResizeListener(new ResizeLayout.OnResizeListener() {
38.
39. public void OnResize(int w, int h, int oldw, int oldh) {
40. int change = BIGGER;
41. if (h oldh) {
42. change = SMALLER;
43. }
44.
45. Message msg = new Message();
46. msg.what = 1;
47. msg.arg1 = change;
48. mHandler.sendMessage(msg);
49. }
50. });
51. }
52. }
这里特别需要注意的是,不能直接在OnResizeListener中对要改变的View进行更改,因为OnSizeChanged函数实际上是运行在 View的layout方法中,如果直接在onSizeChange中改变view的显示属性,那么很可能需要重新调用layout方法才能显示正确。然而我们的方法又是在layout中调用的,因此会出现错误。因此我们在例子中采用了Handler的方法。
1.在lichee/linux-3.0/include/linux/input.h文件可以查看底层驱动的按键编码
#define KEY_F1 59
#define KEY_F2 60
#define KEY_F3 61
#define KEY_F4 62
#define KEY_F5 63
#define KEY_F6 64
#define KEY_F7 65
#define KEY_F8 66
#define KEY_F9 67
#define KEY_F10 68
2.在android4.0/frameworks/base/core/java/android/view/KeyEvent.java文件可以查看上层驱动的按键编码
/** Key code constant: F1 key. */
public static final int KEYCODE_F1 = 131;
/** Key code constant: F2 key. */
public static final int KEYCODE_F2 = 132;
/** Key code constant: F3 key. */
public static final int KEYCODE_F3 = 133;
/** Key code constant: F4 key. */
public static final int KEYCODE_F4 = 134;
/** Key code constant: F5 key. */
public static final int KEYCODE_F5 = 135;
/** Key code constant: F6 key. */
public static final int KEYCODE_F6 = 136;
/** Key code constant: F7 key. */
public static final int KEYCODE_F7 = 137;
/** Key code constant: F8 key. */
public static final int KEYCODE_F8 = 138;
/** Key code constant: F9 key. */
public static final int KEYCODE_F9 = 139;
/** Key code constant: F10 key. */
public static final int KEYCODE_F10 = 140;
3.在/android4.0/frameworks/base/data/keyboards/Generic.kl文件里面有底层到上层的映射,将这个文件定制到自己的系统android4.0/out/target/product/crane-m1003h6/system/usr/keylayout目录下就可以了在上层收到USB键盘F1-F10的消息了。
key 59 F1
key 60 F2
key 61 F3
key 62 F4
key 63 F5
key 64 F6
key 65 F7
key 66 F8
key 67 F9
key 68 F10
4.在/android4.0/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java文件的onKeyDown,onKeyUp函数截获F1-F10的消息,用自己定义的广播消息广播出去应用就可以在后台或者前台都可以接收到按键按下或者释放的广播消息了。
case KeyEvent.KEYCODE_F1: {
//Log.e("zkliu","---------------------------test F1---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F1_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F2: {
//Log.e("zkliu","---------------------------test F2---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F2_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F3: {
//Log.e("zkliu","---------------------------test F3---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F3_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F4: {
//Log.e("zkliu","---------------------------test F4---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F4_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F5: {
//Log.e("zkliu","---------------------------test F5---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F5_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F6: {
//Log.e("zkliu","---------------------------test F6---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F6_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F7: {
//Log.e("zkliu","---------------------------test F7---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F7_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F8: {
//Log.e("zkliu","---------------------------test F8---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F8_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F9: {
//Log.e("zkliu","---------------------------test F9---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F9_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F10: {
//Log.e("zkliu","---------------------------test F10---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F10_KEYDOWN"));
return true;
}
case KeyEvent.KEYCODE_F1: {
//Log.e("zkliu","---------------------------test F1---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F1_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F2: {
//Log.e("zkliu","---------------------------test F2---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F2_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F3: {
//Log.e("zkliu","---------------------------test F3---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F3_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F4: {
//Log.e("zkliu","---------------------------test F4---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F4_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F5: {
//Log.e("zkliu","---------------------------test F5---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F5_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F6: {
//Log.e("zkliu","---------------------------test F6---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F6_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F7: {
//Log.e("zkliu","---------------------------test F7---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F7_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F8: {
//Log.e("zkliu","---------------------------test F8---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F8_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F9: {
//Log.e("zkliu","---------------------------test F9---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F9_KEYUP"));
return true;
}
case KeyEvent.KEYCODE_F10: {
//Log.e("zkliu","---------------------------test F10---------------------------");
getContext().sendBroadcast(new Intent("com.android.action.F10_KEYUP"));
return true;
}
应用程序注册接收相应的广播消息即可接收到键盘输入消息。
InputMethodManager有一个方法isActive(View view):如果view是输入法的活动view,则返回true。也就是说,如果是由view触发弹出软键盘,则返回true。
if(isActive(edittext))
隐藏键盘
接着让另一个view强制获取焦点,这样isActivite(ediitext)就为false.
这个方法比较简单,代码比较短,也很好理解。
附上代码:
InputMethodManager inputMethodManager = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);brprivate boolean hideKeyboard(){
if(inputMethodManager.isActive(searchEditText)){br//因为是在fragment下,所以用了getView()获取view,也可以用findViewById()来获取父控件
getView().requestFocus();//使其它view获取焦点.这里因为是在fragment下,所以便用了getView(),可以指定任意其它view
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
return true;
}
return false;
}
ps: 如果是手动弹出键盘,getActivity().getCurrentFocus()改成searchEditText.并且手动弹出的键盘isActivie()失效,可用标记来判断.
在Microsoft Windows 中,键盘和鼠标是两个标准的用户输入源,在一些交叠的操作中通常相互补充使用。当然,鼠标在今天的应用程序中比10年前使用得更为广泛。甚至在一些应用程序中,我们更习惯于使用鼠标,例如在游戏、画图程序、音乐程序,以及Web创览器等程序中就是这样。然而,我们可以不使用鼠标,但绝对不能从一般的PC中拆掉键盘。 Windows程序获得键盘输入的方式:键盘输入以消息的形式传递给程序的窗口过程。实际上,第一次学习消息时,键盘就是一个明显的例子:消息应该传递给应用程序的信息类型。 Windows用8种不同的消息来传递不同的键盘事件。这好像太多了,但是(就像我们所看到的一样)程序可以忽略其中至少一半的消息而不会有任何问题。并且,在大多数情况下,这些消息中包含的键盘信息会多于程序所需要的。处理键盘的部分工作就是识别出哪些消息是重要的,哪些是不重要的。 键盘基础知识 虽然应用程序在很多情况下可以通过鼠标实现信息的输入,但到现在为止键盘仍然是PC机中不可替代的重要输入设备。 用键盘当作输入设备,每当用户按下或释放某一个键时,会产生一个中断,该中断激活键盘驱动程序KEYBOARD.DRV来对键盘中断进行处理。 KEYBOARD.DRV程序会根据用户的不同操作进行编码,然后调用Windows用户模块USER.EXE生成键盘消息,并将该消息发送到消息队列中等候处理。 1.扫描码和虚拟码 扫描码对应着键盘上的不同键,每一个键被按下或释放时,都会产生一个唯一的扫描码作为本身的标识。扫描码依赖于具体的硬件设备,即当相同的键被按下或释放时,在不同的机器上可能产生不同的扫描码。在程序中通常使用由Windows系统定义的与具体设备无关的虚拟码。在击键产生扫描码的同时,键盘驱动程序KEYBOARD.DRV截取键的扫描码,然后将其翻译成对应的虚拟码,再将扫描码和虚拟码一齐编码形成键盘消息。所以,最后发送到消息队列的键盘消息中,既包含了扫描码又包含了虚拟码。 经常使用的虚拟码在WINDOWS.H文件中定义,常用虚拟码的数值、常量符号和含义如表所示。取值(16进制) 常量符号 含义 01 VK_LBUTTON 鼠标左键 02 VK_RBUTTON 鼠标右键 03 VK_CANCEL Break中断键 04 VK_MBUTTON 鼠标中键 05-07 -- 未定义 08 VK_BACK (BackSpace)键 09 VK_TAB Tab键 0A-0B -- 未定义 0C VK_CLEAR Clear键 0D VK_RETURN Enter键 0E-0F -- 未定义 10 VK_SHIFT Shift键 11 VK_CONTROL Ctrl键 12 VK_MENU Alt键 13 VK_PAUSE Pause键 14 VK_CAPTIAL CapsLock键 15-19 -- 汉字系统保留 1A -- 未定义 1B VK_ESCAPE Esc键 1C-1F -- 汉字系统保留 20 VK_SPACE 空格键 21 VK_PRIOR PageUp键 22 VK_NEXT PageDown键 23 VK_END End键 24 VK_HOME Home键 25 VK_LEFT ←(Left Arrow)键 26 VK_UP ↑(Up Arrow)键 27 VK_RIGHT →(Right Arrow)键 28 VK_DOWN ↓(Down Arrow)键 29 VK_SELECT Select键 2A -- OEM保留 2B VK_EXECUTE Execute键 2C VK_SNAPSHOT Print Screen键 2D VK_INSERT Insert键 2E VK_DELETE Delete键 2F VK_HELP Help键 30-39 VK_0-VK_9 数字键0-9 3A-40 -- 未定义 41-5A VK_A-VK_Z 字母键A-Z 5B-5F -- 未定义 60-69 VK_NUMPAD0-VK_NUMPAD9 小键盘数字键0-9 6A VK_MULTIPLY *(乘号)键 6B VK_ADD +(加号)键 6C VK_SEPAPATOR 分隔符键 6E VK_SUBTRACT -(减号)键 6F VK_DECIMAL .(小数点)键 70-87 VK_DIVIDE /(除号)键 88-8F VK_F1-VK_F24 F1-F24功能键 90 VK_NUMBERLOCK Number lock键 91 VK_SCROLL Scroll lock键 92-B9 -- 未定义 BA-C0 -- OEM保留 C1-DA -- 未定义 DB_E4 -- OEM保留 E5 -- 未定义 E6 -- OEM保留 E7-E8 -- 未定义 E9-F5 -- OEM保留 F6-FE -- 未定义2.输入焦点 同一时刻,Windows中可能有多个不同的程序在运行,也就是说有多个窗口同时存在。这时,键盘由多个窗口共享,但只有一个窗口能够接收到键盘消息,这个能够接收键盘消息的窗口被称为拥有输入焦点的窗口。 拥有输入焦点的窗口应该是当前的活动窗口,或者是活动窗口的子窗口,其标题和边框会以高亮度显示,以区别于其他窗口。拥有输入焦点的也可以是图标而不是窗口,此时,Windows也将消息发送给图标,只是消息的格式略有不同。 窗口过程可以通过发送WM_SETFOCUS和 WM_KILLFOCUS消息使窗体获得或失去输入焦点。程序也可以通过捕获WM_SETFOCUS和WM_KILLFOCUS消息来判断窗体何时获得或失去输入焦点。其中WM_SETFOCUS消息表示窗口正获得输入焦点,WM_ KILLFOCUS消息表示窗口正失去输入焦点。 3.键盘消息 键盘消息分为系统键消息和非系统键消息。系统键消息是指由Aft键和其他键组合而产生的按键消息。当系统键被按下时产生WM_ SYSKEYDOWN消息,当系统键被释放时产生WM_SYSKEYUP消息。 Aft键与其他键形成的组合键通常用于对程序菜单和系统菜单进行选择,或用于在不同的程序之间进行切换。因此,系统键消息应该交由Windows进行处理,用户所编制的程序一般不处理系统键消息,而是将这些消息交由DefWindowProc函数进行处理。如果用户想对系统键消息进行处理,应该在处理完这些消息后,再将其发送给DefWindowProc函数,使得Windows系统能够正常工作。 某些击键消息可以被转换成字符消息,例如字母键、数字键等。而有些键只能产生按键消息而没有字符消息,例如 Shift键、Insert键等。消息循环中的 TranslateMessage函数可以实现从击键消息向字符消息的转化。当GetMessage函数捕获一个WM_SYSKEYDOWN消息或 WM_KEYDOWN消息后,TranslateMessage函数判断产生该消息的键是否能够被转换成字符消息,如果能,就将该消息转换成字符消息,再通过DispatchMessape函数将转换后的字符消息发送到消息队列中去。字符消息共有以下四种,如表所示。字符 系统字符 非系统字符 普通字符 WM_SYSCHAR WM_CHAR 死字符 WM_SYSDEADCHAR WM_DEADCHAR其中死字符是由某些特殊键盘上的按键所造成的,Windows一般忽略死字符所产生的消息。 Windows的消息一般是通过一个MSG结构体变量传送给消息处理函数的。对于键盘消息, MSG结构体变量的各个域中较重要的是lParam域和 wParam域。wParam域用于保存按键的虚拟键代码或字符的ASCII码。对于非字符消息,wParam域保存按键的虚拟健代码;对于字符消息, wParam域不保存字符的ASCII码。lParam域则用于保存击键时产生的附加信息,实际上一个32位的lParam变量被分为六部分,记录了以下相关信息:重复次数、OEM扫描码、扩展键标志、关联键标志、前一击键状态和转换状态。lParam域各位的含义如表所示。位数 含义 0-15 击键重复次数累加 16-23 OEM扫描码 24 是否为扩展键 25-28 未定义 29 是否便用关联键,及Alt键是否同时按下。 30 前一次击键状态,0表示该键前一次状态为抬起,1表示前一次状态为按下 31 转换状态按键的次序不同,产生的消息也不相同。例如,按下并释放1键,读过程依次产生如表所示三条消息。按下1键所产生的消息和wParam的取值消息 wParam变量取值 WM_KEYDOWN 虚拟码1 WM_CHAR ASCII码“1” WM_KEYUP 虚拟码1如果按下Shift键后再按下1键并释放,则依次产生如表所示的消息。按下 Shift键后按 1健所产生的消息和 wParam的取值消息 wParam变量取值 WM_KEYDOWN 虚拟码 VK_SHIFT WM_KEYDOWN 虚拟码 VK_1 WM_CHAR ASCII码 “1” WM_KEYUP 虚拟码 VK_1 WM_KEYUP 虚拟码 VK_SHIF键盘应用实例 下面通过一个应用程序实例来说明在实际编程中如何处理键盘消息。#include windows.h #include stdio.h // 全局变量 RECT rc; //记录滚屏的矩形区域 ? int xChar, yChar; //文本输入点坐标 WNDCLASSEX wnd; //窗口类结构变量 char szAppName[] = "键盘消息监视程序"; //窗口类名 //函数声明 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); BOOL MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE hInstance,int iCmdShow); //函数:WinMain //作用:入口函数 int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR szCmdLine,int iCmdShow) { MSG msg; if(!MyRegisterClass(hInstance)) { return FALSE; }if(!InitInstance(hInstance,iCmdShow)) { return FALSE; }while (GetMessage (msg, NULL, 0, 0)) { TranslateMessage (msg); DispatchMessage (msg); } return msg.wParam; } //函数:ShowKey //作用:实现在窗口中显示按键信息 void ShowKey (HWND hwnd, int iType,char *szMessage,WPARAM wParam,LPARAM lParam) { static char *szFormat[2] ={"%-14s %3d %c %6u %4d %5s %5s %6s %6s", "%-14s %3d %c %6u %4d %5s %5s %6s %6s" }; char szBuffer[80]; HDC hdc; ScrollWindowEx(hwnd, 0, -yChar, rc,rc,NULL,NULL,SW_INVALIDATE); hdc = GetDC (hwnd); SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)); TextOut (hdc,xChar,rc.bottom - yChar,szBuffer,wsprintf szBuffer,szFormat[iType],szMessage, //消息wParam, //虚拟键代码(BYTE) (iType ? wParam :‘ ’),//显示字符值LOWORD (lParam), // 重复次数HIWORD (lParam) 0xFF, // OEM键盘扫描码//判断是否为增强键盘的扩展键(PSTR) (0x01000000 lParam ? “是” : “否”),//判断是否同时使用了ALT键(PSTR) (0x20000000 lParam ? “是” : “否”),(PSTR) (0x40000000 lParam ? “按下” : “抬”),//判断前一次击键状(PSTR)(0x80000000 lParam ? “按下” : “抬起”))//判断转换状态?); ReleaseDC (hwnd, hdc); ? ValidateRect (hwnd, NULL); ? } //函数:WndProc //作用:处理主窗口的消息 LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { static char szTop[] ="消息键 字符 重复数 扫描码 扩展码 ALT 前一状态 转换状态"; static char szUnd[] ="_______ __ ____ _____ ______ ______ ___ _______ ______"; //在窗口中输出文字作为信息标题 HDC hdc; PAINTSTRUCT ps; TEXTMETRIC tm; switch (iMsg) { case WM_CREATE://处理窗口创建的消息 hdc = GetDC (hwnd); //设定字体 SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)); //检取当前字体的度量数据 GetTextMetrics (hdc, tm); xChar = tm.tmAveCharWidth;//保存字体平均宽度 yChar = tm.tmHeight; //保存字体高度 ReleaseDC (hwnd, hdc); rc.top = 3 * yChar / 2; return 0; case WM_SIZE://处理窗口大小改变的消息 //窗体改变后保存新的滚屏区域右下角坐标 rc.right = LOWORD (lParam); rc.bottom = HIWORD (lParam); UpdateWindow (hwnd); return 0; case WM_PAINT: //处理窗口重绘消息 InvalidateRect (hwnd, NULL, TRUE); hdc = BeginPaint (hwnd, ps); SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetBkMode (hdc, TRANSPARENT) ; TextOut (hdc, xChar, yChar / 2, szTop, (sizeof szTop) - 1) ; TextOut (hdc, xChar, yChar / 2, szUnd, (sizeof szUnd) - 1) ; EndPaint (hwnd, ps); return 0; case WM_KEYDOWN: //处理键盘上某一键按下的消息 ShowKey (hwnd, 0, "WM_KEYDOWN",wParam, lParam); return 0; case WM_KEYUP: //处理键盘上某一按下键被释放的消息 ShowKey (hwnd, 0, "WM_KEYUP", wParam, lParam); return 0; case WM_CHAR: //处理击键过程中产生的非系统键的可见字符消息 howKey (hwnd, 1, "WM_CHAR", wParam, lParam); return 0; case WM_DEADCHAR: //处理击键过程中产生的非系统键"死字符"消息 ShowKey (hwnd, 1, "WM_DEADCHAR", wParam, lParam); return 0; case WM_SYSKEYDOWN: //处理系统键按下的消息 ShowKey (hwnd, 0, "WM_SYSKEYDOWN",wParam, lParam); break; case WM_SYSKEYUP: //处理系统键抬起的消息 ShowKey (hwnd, 0, "WM_SYSKEYUP", wParam, lParam); break; case WM_SYSCHAR://处理系统键可见字符消息 ShowKey (hwnd, 1, "WM_SYSCHAR", wParam, lParam); break; case WM_SYSDEADCHAR://处理系统键"死字符"消息 ShowKey (hwnd, 1, "WM_SYSDEADCHAR", wParam, lParam); break; case WM_DESTROY: //处理结束应用程序的消息 PostQuitMessage (0); return 0; } return DefWindowProc (hwnd, iMsg, wParam, lParam); } //函数:MyRegisterClass //作用:注册窗口类 BOOL MyRegisterClass(HINSTANCE hInstance) { wnd.cbSize= sizeof (wnd); wnd.style = CS_HREDRAW CS_VREDRAW; wnd.lpfnWndProc = WndProc; wnd.cbClsExtra = 0; wnd.cbWndExtra = 0; wnd.hInstance = hInstance; wnd.hIcon = LoadIcon (NULL, IDI_APPLICATION);? wnd.hCursor = LoadCursor (NULL, IDC_ARROW); wnd.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); wnd.lpszMenuName = NULL; wnd.lpszClassName = szAppName; wnd.hIconSm = LoadIcon (NULL, IDI_APPLICATION); return RegisterClassEx (wnd); } //函数:InitInstance //作用:创建主窗口 BOOL InitInstance(HINSTANCE hInstance,int iCmdShow) { HWND hwnd; hwnd = CreateWindow (szAppName,"键盘消息监视程序",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,hInstance,NULL); if(!hwnd) { return FALSE; } ShowWindow (hwnd, iCmdShow); UpdateWindow (hwnd); return TRUE; }
macOS开发因为硬件上有鼠标和键盘的支持,所以我们也有监听硬件输入的需求。
监听键盘输入需要使用 NSEvent 这个类,这个类是专门管理“事件”的。 NSEvent 提供了两个监听的方法:
第一个方法是用于监听其他应用中的事件,第二个是用于监听自己应用的时间。更多详细内容可以阅读官方注释。
在这里我们使用第二个进行监听键盘输入:
在方法的 block 回调中会收到当前键盘的事件,你需要通过判断 aEvent.keyCode 来得知点击了哪一个键。macOS有一个枚举来告诉你键的名字是什么,你可以直接拿来判断(文末附)。
NSEvent 的监听是需要进行移除的 ,如果你不进行移除,那么这个需要监听的类再多次 init 后,其 block 也会在键盘点击时被重复的调用多次。
移除需要使用的类为 NSEvent ,方法为:
在 如何监听 那一节,添加监听的方法有一个返回值,这个返回值就是这里 removeMonitor 需要传入的参数。在添加了监听的对象 dealloc 或合适的时候进行调用即可。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流