(纯C + 纯手写 + 手动编译) 一个Windows 窗体应用
本篇文章仅仅是作者的一个类似笔记一样的东西,作为记录。所以请勿出现如下不友善评论
啊,这不是某某IDE直接就可以生成的吗。
搞这个东西有什么意义啊,浪费时间。
正经人谁用Win32 API写窗体程序啊,Qt跨平台他不香嘛。
所以本文章只是记录一下,我遇到的问题和结果问题的过程思路等,或者有对Win32 API
感兴趣的同学,可以参考,学习提出问题一起解决等。
因为只是个笔记,所以教程中可能有些不符合项目代码规范的地方,请谅解。
正文
0x00: 最终效果
本文章采用 MinGW-w64
下的 gcc
和 windres
进行编译链接。
是从SourceForge
上下载的MinGW-W64 GCC-8.1.0
其中 gcc
和 windres
均为内置。
我选择的是x86_64-win32-sjlj (posix 线程模型的应该也可以。)
0x01: 创建一个资源脚本文件 – winGUI.rc
采用 GBK
编码
#include "resource.h"
// 这条是manifest可以解决控件不跟随当前系统样式的问题,具体可以查看我之前发的文章
// ID_MANIFEST RT_MANIFEST "winGUI.exe.manifest"
ID_ICON ICON ".\\icon.ico"
MY_MENU MENU
BEGIN
POPUP "文件(&F)"
BEGIN
MENUITEM "退出(&X)", ID_EXIT
END
POPUP "帮助(&H)"
BEGIN
MENUITEM "关于(&A)", ID_ABOUT
END
END
为了降低文章难度,省去了非必要的manifest环节,可能导致按钮等控件没法跟随系统样式。详细内容可以查看我往期文章的详细内容,自行开启注释掉的代码。
0x02: 创建一个资源文件的头文件 – resource.h
采用 UTF-8
编码
// #define ID_MANIFEST 1000
#define ID_ICON 1001
#define MY_MENU 1010
#define ID_EXIT 1011
#define ID_ABOUT 1012
这里同上,去掉了manifest
相关的宏定义,降低文章理解难度。阅读完成我往期这篇文章,或者高手可自行启用被注释掉的宏定义
0x03: 创建一个资源C语言源文件 – winGUI.c
采用 UTF-8
编码
#include <windows.h>
#include "resource.h"
/* 函数原型 */
// 消息处理函数
LRESULT WINAPI wndProc(HWND, UINT, WPARAM, LPARAM);
// 注册窗口类
void registWindowClass(HINSTANCE);
// 入口函数
int APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPreIns, LPSTR cmdLine, int cmdShow)
{
// 注册窗口类
registWindowClass(hIns);
// 创建窗口句柄
HWND hWnd = CreateWindow("MainWindow", "Title", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
// 显示窗口并更新
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
// 进入消息循环
MSG nMsg = {0};
while (GetMessage(&nMsg, NULL, 0, 0))
{
TranslateMessage(&nMsg);
DispatchMessage(&nMsg);
}
return 0;
}
// 消息处理函数
LRESULT CALLBACK wndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch (msgID)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ID_EXIT:
PostQuitMessage(0);
break;
case ID_ABOUT:
MessageBoxW(hWnd, L"作者: Mycelium", L"关于", MB_OK);
break;
}
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}
// 注册窗口类函数
void registWindowClass(HINSTANCE hIns)
{
WNDCLASS wc = {0};
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hCursor = NULL;
wc.hIcon = LoadIconA(hIns, MAKEINTRESOURCEA(ID_ICON));
wc.hInstance = hIns;
wc.lpfnWndProc = wndProc;
wc.lpszClassName = "MainWindow";
wc.lpszMenuName = MAKEINTRESOURCEA(MY_MENU);
wc.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wc);
}
0x04: 添加图标文件并确保这些源文件在同一个目录
找到一个自己喜欢的图标文件(扩展名是 .ico
)的
我这里找的是 Minecraft材质包里的 png, 用的在线转换网站准换成了 64x64
像素的ico文件
确保四个文件在同一文件夹,再新建两个文件夹用来 存储之后编译生成的中间文件(obj)和最终的可执行文件(bin)
0x05: 开始编译源码
REM 编译资源文件
windres .\winGUI.rc -o .\obj\winGUI.res
REM 编译C语言源文件
gcc -c .\winGUI.c -o .\obj\winGUI.o
REM 链接目标文件
gcc .\obj\winGUI.o .\obj\winGUI.res -o .\bin\winGUI.exe
如果是最后发布程序了,或者不再需要命令行的窗口调试了,可以再添加一个-mwindows
选项来编译出没有 命令行窗口的程序。
如果你启用了manifest相关代码,且了解你在做什么,使用下边的命令代替上边那条来进行链接
gcc .\obj\winGUI.o .\obj\winGUI.res -l ComCtl32 -o .\bin\winGUI.exe
最后双击,或者用命令行启动程序查看成果
.\bin\winGUI.exe
部分问题详解
菜单部分详解
如何自己写一个菜单
遵循下边的语法
${菜单的ID} MENU
BEGIN
POPUP "${顶菜单项}"
BEGIN
MENUITEM "菜单子项", ${菜单项的ID}
END
END
例如
#include "res.h"
MYCELIUM_MENU MENU
BEGIN
POPUP "文件(&F)"
BEGIN
MENUITEM "打开(&O)", ID_OPEN
MENUITEM "退出(&X)", ID_EXIT
END
POPUP "编辑(&E)",
BEGIN
MENUITEM "全选(&A)", ID_SELECT_ALL
END
END
讲解一下这里&字母的作用,是按下alt+后边的字母可以快速定位/点击菜单项的快捷热键。应该挺常见的吧。
比如我想退出可以
先alt+f 再 alt+x
另外还记得新建一个头文件
来用宏定义给这些 ID 一个整数的定义
比如 新建一个res.h
#define MYCELIUM_MENU 1000
#define ID_OPEN 1010
#define ID_EXIT 1011
#define ID_SELECT_ALL 1012
代码层面怎么导入这个菜单的资源呢
在你注册窗口类的时候有个成员叫做 .lpszMenuName
给这个成员一个字符串的数字id就可以了
比如以下的两种方法都可以
但是可能就第一种比较正规。
但是本质上也是一种把数字的id转换成字符串的效果(MAKEINTRESOURCEA这个宏定义函数)(比如123 => “123”)
wc.lpszMenuName = MAKEINTRESOURCEA(MYCELIUM_MENU);
// 或者
wc.lpszMenuName = (char*)MYCELIUM_MENU;
// 当然微软的windows.h头文件里定义的也是可以的
wc.lpszMenuName = (LPSTR)MYCELIUM_MENU;
// 再或者 可以,但是不推荐,不然你新建头文件干啥
//不就是避免给程序员看这些数字id嘛
wc.lpszMenuName = (char*)1000;
当然方法不只在注册时候,挂载这个菜单资源,还有其他的方法和时候,都可以,但是这里只介绍这一种方法,再多的就是Win32课程需要学的了,不是本文章的重点了,所以略过。
还有就是怎么知道用户点击了我的自定义菜单项呢?这就是这个菜单项后边的ID的作用了。本文章通过 消息处理函数来解决的,过于简单直接放源码。
// 消息处理函数
LRESULT CALLBACK wndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch (msgID)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_COMMAND: // <= 重点*
switch (LOWORD(wParam)) // <= 取wParam 的低2字节
{
case ID_EXIT: // <= 你设置的菜单项的ID
PostQuitMessage(0);
break;
case ID_ABOUT:
MessageBoxW(hWnd, L"作者: Mycelium", L"关于", MB_OK);
break;
}
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}
配合上边的 注释应该能理解,我就不过多解释了,也是win32 api相关课程能学到的知识。为什么取低2字节,也是因为windows的消息内容就是这样的。并不是用到了什么技巧。
应用程序的图标
遵循下边的语法
${图标ID} ICON "${图标路径.ico}"
例如:
ID_ICON ICON ".\\icon.ico"
表示 把当前目录下的 icon.ico
文件当作是图标资源 赋予ID ID_ICON。
跟上边的菜单一样,新建一个头文件"res.h"
#define ID_ICON 1005
不修改代码应该就 可以在资源管理器里正常显示了,但是你打开应用,展示给你的窗口左上角的图标应该还没有修改。
同样也只是介绍在注册菜单类时,如何设置窗口的图标
找到.hIcon
给他设置成图标的句柄,就可以完成了,但是仍然推荐使用第一种。感觉比较正规。
例如以下方式:
wc.hIcon = LoadIconA(hIns, MAKEINTRESOURCEA(ID_ICON));
// 或者
wc.hIcon = LoadIconA(hIns, (char*)ID_ICON);
// 或者
wc.hIcon = LoadIconA(hIns, (LPSTR)ID_ICON);
// 使用数字ID的方式这里就不赘述了 可以,但不推荐。
注意LoadIconA
的第一个参数为 当前程序实例句柄
有什么其他的问题可以在评论区留言,我也不一定会,但是没准可以思考讨论一下啥的。