刚刚参加完面试,有些不理想。面试官问的几个有关UI方面的问题没有回答好。我将这几个问题列出来给大家看看。
1.对于窗口等UI资源是属于线程的还是进程的?
2.UI线程创建了一个窗口,能否在另外一个窗口中调用UpdateWindow等函数对窗口进行更新?
我们先看第2个问题,在创建窗口的线程内去调用UpdateWindow肯定是没有问题的,但是在其它的线程中是否可以呢?最好的办法就是写代码验证一下。
我们直接用VS生成一个win32工程,然后创建一个新的线程,在新的线程中调用UpdateWindow。代码很简单,如下:
UINT WINAPI UpdateTreadex(LPVOID lpParameter)
{
HWND hWnd = (HWND)lpParameter;
ShowWindow(hWnd, 1);
UpdateWindow(hWnd);
return 0;
}
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // 将实例句柄存储在全局变量中
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
//ShowWindow(hWnd, nCmdShow);
//UpdateWindow(hWnd);
_beginthreadex(NULL, NULL, UpdateTreadex, hWnd, 0, NULL);
return TRUE;
}
窗口函数中的代码
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: 在此添加任意绘图代码...
TextOut(hdc, 20, 20, _T("123456"), 6);
EndPaint(hWnd, &ps);
break;
如果你看到了一个窗口,以及显示的“123456”,那么恭喜你,你找到正确的答案了。在其它的线程调用UpdateWindow也是生效的。
那么我们不妨思考下面试官问这两个问题是在考察哪些知识呢?
实际上面试官是在考察你对Windows GUI体系的理解程度。下面是我在网上以及一些书籍上整理出来有关Windows GUI体系的知识,希望能帮助到大家。
在早期的Windows NT操作系统中,对显示器的图形操作是由一个服务进程提供,就像是现在Linux上的X Window。但是后来情况发生了变化,微软觉得由服务进程提供图形操作必定导致大量的进程间通信,从而使系统的效率下降。因此从Windws NT 4.0开始,微软把图形操作移到了内核。为此微软在原有的系统调用基础上增加了一组图形相关的系统调用供应用层使用,新增的系统调用我们称之为Win32k扩展系统调用。因此在内核中有两个系统调用表,一个是原来就有的系统调用表,一个是新增后的Win32k扩展系统调用表。
下面我们得讲一下线程的故事了,在内核中每个线程都有一个数据结构KTHREAD来存储线程的信息,在线程的KTHREAD结构中有一个ServiceTable指针,这个指针指向本线程所用的系统调用表。每个线程在创建之初都是使用原本的系统调用表,此后,如果新线程第一次进行了Win32k扩展系统的调用,内核就会将该线程转换成UI线程,并修改ServiceTable指针,使其指向Win32k扩展系统调用表。除了修改ServiceTable指针,操作系统还会为这个线程创建一个消息队列,并将这个消息队列的指针存储到线程的数据结构中。因此我们常说的UI线程,就是比其它普通线程多了一个消息队列,这是关键。那么如何将一个普通线程变成UI线程呢?只要调用一个GUI相关的API就可以了,例如GetMessage()、CreateWindow()。
再说一下窗口吧,窗口也有一个数据结构,里面会存储很多的信息,其中有3个是我们最关注的,一个是指向窗口函数的指针WndProc,一个是创建这个窗口的线程指针OwnerThread,还有一个是指向OwnerThread消息队列的指针MessageQueue。这样当你向一个窗口发送消息时,操作系统就将这个消息放到MessageQueue所指的队列中。因此,你应该明白,只有创建这个窗口的线程才能处理这个窗口的消息。那么一个线程如何处理他所创建的窗口消息呢?答案就是在这个线程中实现消息循环。
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
如果一个UI线程没有实现消息循环,那么就无法处理它所创建的窗口消息了。
对于SendMessage和PostMessage的说明,我曾经错误的以为SendMessage的消息是不会进入到消息队列,在SendMessage内部直接调用了窗口函数,这是不准确的。实际上SendMessage要比这复杂很多,消息队列也不只是有一个消息队列,而是一组的消息队列,其中SendMessage发送的消息存储到一个队列内,PostMessage发送的消息存储到另外一个队列,还有定时器、硬件设备(鼠标、键盘)等各自的消息队列。这样你就明白为什么定时器消息的优先级非常低了,因为GetMessage总是先从其它的消息队列取消息。
我要出去跑步了,下一节详细介绍SendMessage、GetMessage的实现原理。