前言
学习c语言的人是不是有段这样的经历,天天面对黑框框敲来敲去,有木有?不知道是否想过用c写一个窗口程序?我想应该也很希望吧,目前我见过的书中很少有介绍窗口程序设计,如果想写的话,还需要去网上、b站搜索,或者看Window程序设计之类的书,使用c写窗口程序其实挺麻烦的,需要了解Windows消息机制、GDI或者GDI+、还有各种控件,如Button,另外自带的控件都太难看,需要使用GDI或GDI+自己绘制,这样工作量又大了很多,所以现在很少有软件用这种最"底层"的方式实现了,都是各种框架,或者使用C#、WPF,我不曾在c中大量写过,但是在VB中大量写过。
这里你可能有觉得矛盾,VB中需要你自己创建窗口、创建控件?当然不用,全是为了学习Windows机制,在VB中,可以不用新建窗口,通过sub main方式启动,在方法中调用大量系统API,完成创建窗口,窗口控件,以及控件的事件响应,这样下来也上百行了,工作量还是很大的。
另外也可以使用别的语言实现,如C#,通过调用系统API,创建窗口等,Java也可以调用Windows API,但是使用相关的API创建窗口,我并没试过(不是说JFrame)。有时间试试。
(其实不太喜欢做Java,我喜欢研究Windows,奈何当初选错了路,导致现在不能回头,如果在回头后会浪费大量时间重新学习,小伙伴做选择要慎重哦!!!也希望有共同爱好的小伙伴一起交流)
实现过程
今天的主角是汇编实现,代码量也不少,原理是invoke调用大量API完成,创建窗口不管用什么语言实现,原理都一样,先注册一个窗口类、窗口调用CreateWindow创建窗口,然后在窗口过程函数中不断处理消息即可(前提这个语言能调用Win Api)。
下面介绍三个必要的函数。
RegisterClassEx
如果要创建一个创建,首先必须调用RegisterClassEx注册一个窗口类,他的参数只有一个,WNDCLASSEX结构体,但是这个结构体有点复杂,成员非常多。
typedef struct WNDCLASSEXA {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;
这里不全部介绍了,但是有一个很重要的需要说一下,就是lpfnWndProc成员,指向一个函数过程,这就牵扯出Windows的消息机制。
窗口过程函数
这个不太好理解,但是写过一次Windows程序的人就明白了,当我们操作一个窗口时,不论是单机、双击、右击、鼠标移动、还是任何你发出的事件,它就会接收到系统发过来的消息,消息中就会标识这个事件是什么,然后我们需要根据消息标识做出不同响应,而就需要我们写一个窗口过程函数,告诉系统,发生消息时,你给我回调到这个地方,如果在这个函数中不处理消息,就可以调用DefWindowProc函数来处理,这是系统的默认消息处理函数。
这个函数过程的参数也是固定的,在官方文档中是这样定义的,这里面又会牵扯出句柄的概念,简单说一下,句柄就是标识一个窗口的一个整数,就像身份证号一样。参数uMsg就是消息标识,也是个整数,系统把他们都定义好了,常量名都是WM_开头得,如WM_MOVE,在窗口移动窗口时系统会通知我们。
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
CreateWindowEx
创建一个窗口,返回窗口句柄,参数都比较简单,这里最有意思的是窗口的样式参数,有非常多的样式可选择,平常我们是见不到这些样式的。
HWND CreateWindowExA(
DWORD dwExStyle,
LPCSTR lpClassName,
LPCSTR lpWindowName,
DWORD dwStyle,
int X,
int Y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
CreateWindow完成之后,就需要不断循环从消息队列获取消息,进行分发。
好,上代码!!!
.386
.model flat,stdcall
option casemap:none
include c:\masm32\include\windows.inc
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
include c:\masm32\include\kernel32.inc
includelib c:\masm32\lib\kernel32.lib
WinMain proto :DWORD
.DATA
ClassName db "MyWindowClass",0
AppName db "汇编实现窗口",0
.DATA?
hInstance HINSTANCE ?
.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance
invoke ExitProcess, eax
WinMain proc hInst:HINSTANCE
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
0,\
0,\
300,\
300,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWDEFAULT
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
WndProc endp
end start
运行后,得出个窗口,使用了80多行。
判断鼠标单击
接下来,我们做个简单的修改,当鼠标单击窗口的时候,在窗口打印"Hello GUI",但是这里面又涉及到HDC的知识,小伙伴自行了解哦。
.386
.model flat,stdcall
option casemap:none
include c:\masm32\include\windows.inc
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
include c:\masm32\include\kernel32.inc
includelib c:\masm32\lib\kernel32.lib
WinMain proto :DWORD
.DATA
ClassName db "MyWindowClass",0
AppName db "汇编实现窗口",0
HelloGUI db "HelloGUI",0
.DATA?
hInstance HINSTANCE ?
hWindowHdc HDC ?
.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance
invoke ExitProcess, eax
WinMain proc hInst:HINSTANCE
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, ADDR wc
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
0,\
0,\
300,\
300,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke GetDC,eax
mov hWindowHdc,eax
invoke ShowWindow, hwnd,SW_SHOWDEFAULT
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL strRect:RECT
.IF uMsg == WM_LBUTTONDOWN
mov strRect.right,100
mov strRect.bottom,100
mov strRect.left,0
mov strRect.top,0
invoke DrawText,hWindowHdc, ADDR HelloGUI,8,ADDR strRect,DT_VCENTER
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
ret
WndProc endp
end start
效果:
创建按钮
创建控件其实也是通过CreateWindowEx来完成,并不需要我们注册类名,使用系统定义好的类名即可,这部分可以查看文档:https://docs.microsoft.com/en-us/windows/win32/winmsg/about-window-classes。
最终还要给按钮设置WS_TABSTOP OR WS_VISIBLE OR WS_CHILD OR BS_DEFPUSHBUTTON
这几个样式,其中WS_CHILD
样式代表作为一个子窗口,否则会出现意料之外的。
当按钮单击后,系统会回调WM_COMMAND消息, wParam 低两字节是命令ID,这就需要在CreateWindowEx时候指明。
.386
.model flat,stdcall
option casemap:none
include c:\masm32\include\windows.inc
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
include c:\masm32\include\kernel32.inc
includelib c:\masm32\lib\kernel32.lib
WinMain proto :DWORD
.DATA
ClassName db "MyWindowClass",0
AppName db "汇编实现窗口",0
ButtonClassName db "Button",0
ButtonTitle db "按钮",0
HelloGUI db "HelloGUI",0
Message db "单机了按钮",0
MessageTitle db "提示",0
.DATA?
hInstance HINSTANCE ?
hWindowHdc HDC ?
hButton HWND ?
hWindow HWND ?
.CONST
ButtonID equ 1
.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance
invoke ExitProcess, eax
WinMain proc hInst:HINSTANCE
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, ADDR wc
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
0,\
0,\
300,\
300,\
NULL,\
NULL,\
hInst,\
NULL
mov hWindow,eax
invoke CreateWindowEx,NULL,ADDR ButtonClassName,ADDR ButtonTitle, WS_TABSTOP OR WS_VISIBLE OR WS_CHILD OR BS_DEFPUSHBUTTON ,\
0,0,100,100,hWindow,ButtonID,NULL,NULL
mov hButton,eax
invoke GetDC,eax
mov hWindowHdc,eax
invoke ShowWindow, hWindow,SW_SHOWDEFAULT
invoke UpdateWindow, hWindow
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL strRect:RECT
.IF uMsg == WM_LBUTTONDOWN
mov strRect.right,100
mov strRect.bottom,100
mov strRect.left,0
mov strRect.top,0
invoke DrawText,hWindowHdc, ADDR HelloGUI,8,ADDR strRect,DT_VCENTER
.ELSEIF uMsg ==WM_COMMAND
mov eax,wParam
.IF ax == ButtonID
invoke MessageBox,hWindow,ADDR Message,ADDR MessageTitle,MB_OK
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
ret
WndProc endp
end start
怎么样,学废了吗?