最近勉强找到了各种利用win32 API的方式,终于能有更多交互性。
虽说学会写程序后基本上一直在写控制台程序,但今天才发现控制台的很多功能都一直没用过,或者说即使用过也没深入了解。
1、中间程序:利用IDE启动和手动启动可执行文件的区别
首先是IDE与控制台之间的关系:以(恶名昭著)的Dev C++举例,IDE调用编译器生成可执行文件a.exe后,并不是直接的调用a.exe,而是有一个中间程序:
Dev C++先调用ConsolePauser.exe,并将目标程序路径传递给后者,这样我们在资源管理器中双击a.exe执行或批处理脚本启动能实现几个功能:a.exe返回退出时,并不直接关闭控制台(新手必问运行程序一闪而过看不到执行结果),而是主函数返回后system("pause")等待,顺便还可以显示主程序返回值与程序运行时间:
Process exited after 1.314 seconds with return value 0
请按任意键继续. . .
ConsolePauser.exe功能并不复杂,针对性能分析也可以自己制作魔改版本:
(没编译成功好菜啊我)(小声)直接拿来主义吧:
-----------------------------------------------
执行时间:114.514 ms
最大内存使用:1919810 KB
程序返回值:0 (0x0)
-----------------------------------------------
请按任意键继续. . .
VS也有类似的中间程序VsDebugConsole.exe,根据项目配置不同也可以选择是否直接调用。控制台自己的中间程序作为跳板启动目标程序,通常点击控制台标题栏菜单-属性对话框的标题栏就是中间程序的路径。
2、控制台属性的默认值与保存位置
知道了通过IDE启动与手动启动时,两者的主程序并不是同一个,我们继续——
瞎搞cmd设置时,注意到有时cmd默认设置并不会影响到IDE启动的控制台属性。尤其是默认代码页:一段时间内我的Dev C++启动的语言区永远是436美国区==>中文永远是乱码。
注册表中HKEY_CURRENT_USER\Console 下会存储不同路径的控制台程序的用户设置,例如:.\%SystemRoot%_system32_cmd.exe 与C:_Program Files (x86)_Dev-Cpp_org_ConsolePauser.exe 项分别存储了系统自带cmd控制台和DevC++的中间程序的用户设置,诸如ScreenBufferSize屏幕缓冲区大小、WindowSize窗口大小、CodePage默认代码页、QuickEdit快速编辑模式、InsertMode插入编辑等等选项。缺省项则使用默认设置。删除该文件夹下键值可以恢复默认设置。
因此只要手动更改属性页中设置,变更项目就会存储在顶层所在的注册表里,并影响下次启动。程序变更如system("chcp ***")、SetWindowInfo()等等不会影响用户设置。且不同控制台程序之间不通用。
3、各种更改控制台属性的方式
好了,我们现在拥有至少三种设置属性的方式:手动更改、注册表修改、程序更改。程序更改还可以分为使用winapi函数和system("")控制台命令更改。
手动更改页面如下,可以选择更改默认值或当前属性,虽然更改当前属性也会保存为用户设置影响下次启动就是了:
3.1代码页
代码页即字符编码区,主要影响宽字符的显示。常用的有:
437 | OEM-美国 |
936 | ANSI/OEM - 简体中文(GBK) |
65001 | UTF-8 |
对应注册表项:CodePage
控制台命令:chcp ***
WIN API函数:待补充
3.2快速编辑与插入模式
快速编辑,可以框选控制台文字,鼠标右键控制复制/粘贴(毕竟Ctrl + C/V 在这里失效了),似乎从win10之后变成了默认开的状态。在框选模式下程序会pause等待暂停运行,标题栏会提示“选定”。
插入模式,我用的不多只知道控制台是覆盖后面字符的,对应Insert键切换。
对应注册表项:QuickEdit和InsertMode。键值
控制台命令:待补充
WIN API函数,使用标志bit位控制:
//修改窗口属性
void DisableConsoleQuickEdit(){
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin, &mode);
mode &= ~ENABLE_QUICK_EDIT_MODE; //移除快速编辑模式
mode &= ~ENABLE_INSERT_MODE; //移除插入模式
mode &= ~ENABLE_MOUSE_INPUT;
SetConsoleMode(hStdin, mode);
//原文链接:https://blog.youkuaiyun.com/weixin_43229139/article/details/119533131
}
3.3屏幕缓冲区大小、窗口大小
控制台命令:mode con cols={宽度} lines={高度}。会同时更改屏幕缓冲区大小与窗口大小。
注册表项:ScreenBufferSize屏幕缓冲区大小、WindowSize窗口大小,类型为REG_DWORD,应该是分别按位存储,HIWORD存储高度HIWORD存储宽度,高度单位为行,宽度单位为字符。
WIN API函数:
SetConsoleWindowInfo 函数 - Windows Console | Microsoft Learn
SetConsoleScreenBufferSize 函数 - Windows Console | Microsoft Learn
这位更是重量级,在弄清约束之前设置失败是常有的事:
3.3.1基础语法
两者分别使用不同的函数,甚至数据格式都不一样:
//设置窗口大小需要的类型,表示窗口左上角与右下角两个点的坐标
typedef struct _SMALL_RECT {
SHORT Left;
SHORT Top; // 前两个成员代表窗口左上角坐标,勾选由"系统定位窗口"时该值为(0, 0)
SHORT Right;
SHORT Bottom; // 后两个成员代表窗口右下角坐标,两点共同确定窗口位置、大小
} SMALL_RECT, *PSMALL_RECT;
//调用格式
SetConsoleWindowInfo(
HANDLE, //自己的窗口标准输出句柄,通过GetStdHandle(STD_OUTPUT_HANDLE)获得
BOOL, //没看懂MSDN的描述(个人理解和表现对不上),一般为true就好了
*SMALL_RECT); // 设置值
//设置屏幕缓冲区大小需要的类型
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
//调用格式
SetConsoleScreenBufferSize(
HANDLE hStdout, //自己的窗口标准输出句柄,通过GetStdHandle(STD_OUTPUT_HANDLE)获得
*COORD); // 设置值
可视化界面中看到高度、宽度的单位均为“长度” ,而c系一如既往从0计数,所以例如想要设置大小为(80, 35),需要向函数装入的是(79, 34)
3.3.2两者的约束关系
玩弄可视化设置界面时发现这两者会相互影响,两个维度都遵守最基本的约束关系:
窗口大小
屏幕缓冲区大小
可视化设置页面会自动更正另外一边不符合要求的参数。但到了程序上就没这么简单了,两个参数需要单独设置,新参数与另一个旧参数进行比对,而其中一维违反约束时会导致设置失败(函数返回0)。那么单从其中一维来看,一个精简正确的流程伪代码如下:
// 假设输入参数满足 新窗口大小 <= 新屏幕缓冲区大小
BOOL 设置大小(SHORT 新窗口大小, SHORT 新屏幕缓冲区大小){
if (新窗口大小 <= 原有屏幕缓冲区大小){
设置(新窗口大小);
设置(新屏幕缓冲区大小);
}
else{
设置(新屏幕缓冲区大小);
设置(新窗口大小);
}
return true;
}
但高度和宽度是需要同时设置的,其中一个维度违反设置都会导致失败。
3.3.3正确的设置姿势
我还没想出来一个能在两次设置内成功解决的办法,三次设置倒是可以,甚至不需要自己进行复杂的逻辑判断:
// 获得当前窗口大小 出处 https://blog.dotcpp.com/a/7363
SMALL_RECT SizeOfWindow(HANDLE hConsoleOutput)
{
CONSOLE_SCREEN_BUFFER_INFO info;
BOOL ret = GetConsoleScreenBufferInfo(hConsoleOutput, &info);
return info.srWindow;
}
BOOL set(HANDLE hStdout, const *SMALL_RECT SetWindowSize, const *COORD SetScrBuffSize){
//读取原有屏幕缓冲区大小,当然也可以先设置屏幕大小,但改动两次窗口大小可能会被观察到闪
OldWindowSize = SizeOfWindow(hStdout);
COORD TempScrBuffSize = {max(OldWindowSize.Right, SetWindowSize.Right), max(OldWindowSize.Bottom, SetWindowSize.Bottom)};
SetConsoleScreenBufferSize(hStdout, TempScrBuffSize); // 使屏幕缓冲区大小更改为最大可能,确保屏幕大小成功设置
SetConsoleWindowInfo(hStdout, true, SetWindowSize); // 设置屏幕大小
SetConsoleScreenBufferSize(hStdout, SetScrBuffSize); // 设置屏幕缓冲区大小
}
大概思路如上,详细(半)完备代码可见我的另一篇文章:
https://blog.youkuaiyun.com/2401_83950228/article/details/138104663?spm=1001.2014.3001.5502
3.4控制台也想用Windows窗口基本功能API
这里是终于想和其他程序联动的部分,进一步减轻操作负担。(好吧我在找这些是在写挂机脚本)
3.4.1获取其他窗口句柄:
WINUSERAPI
HWND // 返回窗口句柄
WINAPI
FindWindowA(
_In_opt_ LPCSTR lpClassName, // 类名,可以使用spy++查找捕获窗口后查看,可置空为NULL
_In_opt_ LPCSTR lpWindowName); // 窗口标题,不清楚不标题唯一时如何获得多个创空句柄
WINUSERAPI
HWND
WINAPI
FindWindowW(
_In_opt_ LPCWSTR lpClassName,
_In_opt_ LPCWSTR lpWindowName);
#ifdef UNICODE // 是否支持宽字符
#define FindWindow FindWindowW
#else
#define FindWindow FindWindowA
#endif // !UNICODE
3.4.2控制台获得自己的句柄 + 控制窗口最大最小化
http:// https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-showwindowhttp:// https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-showwindow
两个功能揉在一起了。毕竟目前没有大规模开发打算。
// 秀一秀你自己 方法参考https://learn.microsoft.com/zh-cn/troubleshoot/windows-server/performance/obtain-console-window-handle
void ShowYourself(int mode = SW_SHOWNORMAL){
char WindowTitle[1024];
GetConsoleTitle(WindowTitle, sizeof(WindowTitle));
HWND h = FindWindow(NULL, WindowTitle);
//std::cout << "ShowYourself():\t" << h << "visible\t" << IsWindowVisible(h) << '\n';
if (h == 0) {
std::cout << "找不到自己!的句柄\n";
return;
}
ShowWindow(h, mode); // 可用选项:https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-showwindow
//SW_MINIMIZE: 最小化
}
顺便检查窗口是否可见:
WINUSERAPI BOOL WINAPI IsWindowVisible(_In_ HWND hWnd);
该可见指的大概是能在底部任务栏看见,不可见窗口在spy++中会显示为灰色。
3.4.2检查窗口句柄是否有效
WINUSERAPI BOOL WINAPI IsWindow(_In_opt_ HWND hWnd);
仅用于检查是否为窗口,注意不同的窗口可能拥有相同的句柄,必要时需要检查名称。下面是获得窗口句柄的类名,我也不知道注释提示是类型名不是类是什么意思。可用FindWindow的第一个返回参数lpClassName。
/* * This gets the name of the window TYPE, not class. This allows us to
* recognize ThunderButton32 et al. */
WINUSERAPI UINT WINAPI RealGetWindowClassA(
_In_ HWND hwnd,
_Out_writes_to_(cchClassNameMax, return) LPSTR ptszClassName,
_In_ UINT cchClassNameMax);
/* * This gets the name of the window TYPE, not class. This allows us to
* recognize ThunderButton32 et al. */
WINUSERAPI UINT WINAPI RealGetWindowClassW(
_In_ HWND hwnd,
_Out_writes_to_(cchClassNameMax, return) LPWSTR ptszClassName,
_In_ UINT cchClassNameMax);
#ifdef UNICODE
#define RealGetWindowClass RealGetWindowClassW
#else
#define RealGetWindowClass RealGetWindowClassA
#endif // !UNICODE
3.5其他杂项
更改窗口标题栏 SetConsoleTitle("我是标题");
以及其他还没有用到的功能:
C++控制台的那些【高级操作】(全)_c++调整窗口大小-优快云博客
C语言调整控制台颜色、大小、标题、文字位置、窗口位置、按钮-优快云博客
把功能调完就去睡觉了。一个人在小黑屋里敲代码调功能好无聊,被bug折磨的一点没脾气。
说着去睡觉大概要去西半球睡了吧=_=