用托盘处理控制台程序显隐

近期有任务写一个Windows服务,要求可以没有界面,但要能看到运行情况,并可以缩小在后台运行。对MFC还不够熟悉,又比较懒不想再用C#封一遍,干脆写成控制台程序+托盘图标应用,实现点击托盘可显隐控制台窗体,右键菜单退出。

直接上代码

折腾的时间加上碰到的各种坑,可以说是比写一个窗体程序花的时间还要长了,但也算是有收获,解决各种问题的过程和心路历程都放在注释里了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
bool isDisplay = false;//默认隐藏控制台
HMENU hmenu;//托盘右键菜单,初始化要放全局
NOTIFYICONDATA nid;//托盘句柄

//托盘图标事件回调
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
POINT pt;//用于接收鼠标坐标
int opt;//用于接收菜单选项返回值

switch (message)
{
case WM_CREATE:
//初始化NOTIFYICONDATA结构
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hwnd;//这里句柄不是控制台,而是依赖于空窗体,直接依赖控制台的话,不知道怎么注册事件回调
nid.uID = 0;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_USER;
nid.hIcon = LoadIcon(GetModuleHandle(0), MAKEINTRESOURCE(IDI_ICON2));
wcscpy_s(nid.szTip, make_text(0));
Shell_NotifyIcon(NIM_ADD, &nid);
//提工右键菜单退出
hmenu = CreatePopupMenu();
AppendMenu(hmenu, MF_STRING, TRAY_MENU_EXIT, _T("退出"));
break;

case WM_USER:
if (lParam == WM_LBUTTONDOWN) {
if (isDisplay) {
ShowWindow(GetConsoleWindow(), SW_HIDE);
isDisplay = false;
}
else {
ShowWindow(GetConsoleWindow(), SW_SHOW);
isDisplay = true;
}
}
if (lParam == WM_RBUTTONDOWN)
{
GetCursorPos(&pt);//取鼠标坐标
SetForegroundWindow(hwnd);//解决在菜单外单击左键菜单不消失的问题
opt = TrackPopupMenu(hmenu, TPM_RETURNCMD, pt.x, pt.y, NULL, hwnd, NULL);//显示菜单并获取选项ID
if (opt == TRAY_MENU_EXIT) {
auto ret = MessageBox(hwnd, _T("确认退出?"), FORM_NAME, MB_OKCANCEL | MB_ICONQUESTION);
if (IDOK == ret) {
SendMessage(hwnd, WM_CLOSE, wParam, lParam);
}
}

if (opt == 0)
PostMessage(hwnd, WM_LBUTTONDOWN, NULL, NULL);
}
break;

case WM_DESTROY:
Shell_NotifyIcon(NIM_DELETE, &nid);
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

BOOL WINAPI ConsoleHandler(DWORD CEvent)
{
//不做设置的话只有鼠标滑过才会消失
switch (CEvent)
{
case CTRL_BREAK_EVENT:
Shell_NotifyIcon(NIM_DELETE, &nid);
break;
case CTRL_CLOSE_EVENT:
Shell_NotifyIcon(NIM_DELETE, &nid);
break;
case CTRL_SHUTDOWN_EVENT:
Shell_NotifyIcon(NIM_DELETE, &nid);
break;
}
return TRUE;
}

//创建托盘图标
void CreateTray()
{
MSG msg;
WNDCLASS wndclass;
//若句柄已经存在则提示正在运行,防止多次打开
HWND handle = FindWindow(NULL, CONSOLE_NAME);
if (handle != NULL)
{
MessageBox(NULL, TEXT("服务正在运行中!"), FORM_NAME, MB_ICONERROR);
return;
}
//设置窗体属性
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = 0;
wndclass.hIcon = LoadIcon(GetModuleHandle(0), MAKEINTRESOURCE(IDI_ICON1));
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = FORM_NAME;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
FORM_NAME, MB_ICONERROR);
return;
}

//创建一个空窗体用来创建托盘及事件回调,不知道有没有方法能直接处理控制台
CreateWindowEx(WS_EX_TOOLWINDOW,
FORM_NAME, FORM_NAME,
WS_POPUP,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL, NULL, 0, NULL);

HWND console = GetConsoleWindow(); //获取控制台窗口句柄
SetConsoleTitle(CONSOLE_NAME); //设置控制台窗体名称
SetConsoleCtrlHandler(ConsoleHandler, TRUE);//设置下控制台关闭事件,主动销毁托盘图标
ShowWindow(console, SW_HIDE); //默认隐藏控制台
UpdateWindow(console);

while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

可继续改进

本质上,托盘还是依赖于了一个空窗体句柄,没有找到比较完美的能直接给控制台句柄添加事件回调的方式,虽然处理了控制台关闭等事件,但并不能处理托盘图标点击事件。估计HOOK方式是可以的,并且这里就可以直接hook控制台,处理最小化和关闭都为隐藏,完全由托盘图标控制关闭,不过目前达到了我的需求,就只做到了上面这一步,控制台按钮还都是原本的功能。

本以为一个简单的功能,在有参考的情况下还是折腾了小半天,记录一下加深印象。

Powered by Hexo

Copyright © 2018 - 2022 Yshen's Blog All Rights Reserved.

UV : | PV :

Fork me on GitHub