当前位置: 首页 > news >正文

VC++隐藏DOS窗口:从原理到实践的4种核心方法

1. 项目概述:为什么我们需要隐藏DOS窗口?

在VC++开发中,尤其是开发一些后台服务、系统工具或者带有图形界面的程序时,我们经常会遇到一个不大不小但很恼人的问题:一个黑色的DOS控制台窗口一闪而过,或者干脆就杵在那里。对于最终用户来说,这个窗口不仅不美观,还可能引起困惑,甚至被误认为是病毒或错误。因此,掌握隐藏DOS窗口的方法,是提升程序专业度和用户体验的一个基本功。

这个问题看似简单,但背后涉及到Windows程序的不同入口点、链接器设置以及进程创建机制等多个层面的知识。很多新手开发者可能会尝试一些“治标不治本”的方法,比如用system(“pause”)来阻塞,或者用WinExec运行命令,但往往效果不佳或不够优雅。本文将从一个资深VC++开发者的角度,彻底拆解隐藏DOS窗口的几种核心方法,从原理到实操,并分享一些我踩过的坑和独家技巧。

2. 核心原理:Windows程序的两种“面孔”

要隐藏DOS窗口,首先得明白它为什么会出来。这得从Windows程序的两种基本类型说起。

2.1 控制台程序 vs. Windows图形界面程序

VC++(或者说Visual Studio)默认创建的项目类型主要有两种:控制台应用程序Windows桌面应用程序。它们的根本区别在于程序的入口函数和子系统。

  • 控制台应用程序:入口函数是main()wmain()。它链接到控制台子系统。当操作系统启动这样一个程序时,它会自动为其分配一个控制台(即那个黑底白字的DOS窗口)。即使你的程序里调用了Windows API创建了图形界面,这个控制台窗口通常也会存在,除非你采取特殊手段隐藏它。很多教程里写的“Hello World”程序就是这种。
  • Windows桌面应用程序:入口函数是WinMain()wWinMain()。它链接到Windows子系统。这种程序被设计为纯粹的图形界面程序,系统不会为其创建控制台窗口。它的所有输入输出通常通过窗口消息和图形界面完成。

所以,最根本、最彻底的“隐藏”方法,其实就是让你的程序变成一个真正的Windows桌面应用程序。这样,从根源上就杜绝了控制台窗口的产生。

2.2 链接器子系统设置:项目的“身份证”

在Visual Studio的项目属性里,这个“身份”是由链接器的子系统选项决定的。你可以通过以下路径查看和修改:项目属性 -> 链接器 -> 系统 -> 子系统这里常见的选项有:

  • 控制台 (/SUBSYSTEM:CONSOLE):对应控制台程序。
  • Windows (/SUBSYSTEM:WINDOWS):对应Windows图形界面程序。

很多隐藏窗口的“奇技淫巧”,本质上都是在和这个子系统设置以及入口函数“斗智斗勇”。

3. 方法一:釜底抽薪——修改项目类型与入口函数

这是最推荐、最干净的方法,适用于你从头开始开发一个不希望有任何控制台窗口的程序。

3.1 将控制台项目改为Windows项目

假设你一开始错误地创建了一个“控制台应用程序”,但后来想把它变成无窗口的Windows程序。

  1. 修改链接器子系统: 打开项目属性,定位到链接器 -> 系统 -> 子系统,将值从“控制台 (/SUBSYSTEM:CONSOLE)” 改为 “Windows (/SUBSYSTEM:WINDOWS)”。

  2. 修改入口函数: 将你的源代码中的main()函数改为WinMain()函数。WinMain的函数签名如下:

    int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { // 你的代码 return 0; }

    如果你习惯使用多字节字符集(而非Unicode),则使用WinMain

    int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
  3. 处理入口点冲突: 改了子系统后,链接器会寻找WinMain作为入口。如果你只改了设置没改函数名,或者项目中有多个可能的入口函数,会导致链接错误LNK2019: 无法解析的外部符号 _WinMain@16

    • 解决方案A:确保有且仅有一个正确的WinMain函数。
    • 解决方案B:如果你出于某些原因必须保留main函数,可以在链接器 -> 高级 -> 入口点中手动指定入口函数为main。但这是一种“混合模式”,不推荐初学者使用,可能会遇到一些运行时库的初始化问题。

实操心得:我强烈建议在创建新项目时就选对类型。如果是要开发一个工具类程序,没有UI,但又不想有窗口,可以选择“Windows桌面向导”,然后选择“空项目”,这样生成的就是一个子系统为Windows、入口为WinMain的空项目,非常干净。

3.2 使用#pragma comment指令动态指定

如果你不想动项目属性,或者需要编写一些跨项目配置的通用代码,可以在源代码中使用编译指令来告诉链接器。

#pragma comment(linker, "/subsystem:windows /entry:mainCRTStartup")

这行代码放在main函数所在的源文件中(通常是.cpp文件的开头),作用等同于在链接器设置中指定了/SUBSYSTEM:WINDOWS/ENTRY:mainCRTStartup

  • /subsystem:windows:将程序设置为Windows子系统。
  • /entry:mainCRTStartup:指定程序的入口点为mainCRTStartup。这是C运行时库的初始化函数,它最终会调用你的main函数。通过这种方式,我们绕过了链接器对WinMain的寻找,允许我们在Windows子系统程序中使用main函数。

注意事项:这种方法虽然方便,但属于一种“非标准”用法。它依赖于Microsoft VC++的特定实现。在某些复杂的场景下,可能会与某些库的初始化顺序产生微妙的冲突。对于生产环境的关键程序,优先推荐修改项目属性的标准方法。

4. 方法二:暗度陈仓——创建进程时隐藏窗口

这是另一种非常常见的需求:你的主程序本身可能是一个图形界面程序(无控制台),但你需要在这个程序内部,调用另一个外部命令行程序(如ping.exe,netstat.exe,或一个你自己写的控制台工具),并且希望这个被调用的程序“静默”运行,不弹出黑窗口。

这时,我们就需要用到进程创建API,并对新进程的窗口状态进行控制。核心函数是CreateProcess

4.1 CreateProcess 与 STARTUPINFO 结构

CreateProcess函数功能强大,参数众多。其中,lpStartupInfo参数是一个指向STARTUPINFOSTARTUPINFOEX结构的指针,这个结构体专门用于指定新进程的主窗口特性。

关键字段在于dwFlagswShowWindow

  • dwFlags:需要包含STARTF_USESHOWWINDOW标志,这样系统才会使用wShowWindow成员的值。
  • wShowWindow:设置窗口的初始显示状态。要隐藏窗口,就将其设为SW_HIDE

下面是一个标准的调用示例:

#include <windows.h> #include <tchar.h> void RunCommandHidden(const TCHAR* cmdLine) { STARTUPINFO si = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pi = { 0 }; si.dwFlags = STARTF_USESHOWWINDOW; // 告诉系统使用wShowWindow si.wShowWindow = SW_HIDE; // 关键:隐藏窗口 // 注意:CreateProcess的第一个参数可以为NULL,第二个参数需要可写缓冲区 TCHAR writableCmdLine[MAX_PATH]; _tcscpy_s(writableCmdLine, cmdLine); BOOL bSuccess = CreateProcess( NULL, // 应用程序名(如果为NULL,则在命令行中解析) writableCmdLine, // 命令行(需要可修改的缓冲区) NULL, // 进程安全属性 NULL, // 线程安全属性 FALSE, // 句柄继承选项 CREATE_NO_WINDOW, // 创建标志:这是另一个关键点!见下文分析 NULL, // 环境变量块 NULL, // 当前目录 &si, // 启动信息(包含了SW_HIDE) &pi // 返回的进程信息 ); if (bSuccess) { // 通常我们会等待进程结束,并关闭句柄以避免资源泄露 WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { // 处理错误,可以用GetLastError()获取错误码 DWORD err = GetLastError(); _tprintf(_T("CreateProcess failed (%d).\n"), err); } } // 调用示例 int main() { RunCommandHidden(_T("ping 127.0.0.1 -n 5")); return 0; }

4.2 CREATE_NO_WINDOW 标志的妙用

在上面的代码中,你可能会注意到CreateProcessdwCreationFlags参数我传递了CREATE_NO_WINDOW。这个标志非常强大,它的官方解释是:为新进程创建一个没有控制台窗口的控制台。它比STARTF_USESHOWWINDOW | SW_HIDE更底层、更彻底。

  • SW_HIDE:是“显示”层面的隐藏。窗口实际上被创建了,然后立即被隐藏。在某些极快的情况下,你可能还是会看到窗口“闪一下”。
  • CREATE_NO_WINDOW:是“创建”层面的阻止。系统根本就不会为这个控制台进程分配和创建可见窗口。从根源上杜绝了“闪烁”的可能。

踩坑实录:早期我经常只使用SW_HIDE,在性能好的机器上没问题,但在一些老旧的电脑上,或者当被调用程序初始化很快时,那个黑窗口还是会顽强地“闪现”一下,用户体验很不好。后来统一改用CREATE_NO_WINDOW,这个问题就彻底解决了。所以,最佳实践是:两者结合使用,既设置si.wShowWindow = SW_HIDE,又在dwCreationFlags中加上CREATE_NO_WINDOW,双保险。

4.3 重定向输入输出与隐藏窗口的结合

很多时候,我们隐藏窗口不仅仅是为了好看,还为了能悄悄地获取命令行程序的输出结果,或者向它发送输入。这就涉及到标准输入、输出、错误句柄的重定向。

我们需要在STARTUPINFO中设置hStdInput,hStdOutput,hStdError,并将dwFlags加上STARTF_USESTDHANDLES

void RunAndGetOutput(const TCHAR* cmdLine) { SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE }; HANDLE hStdOutRead, hStdOutWrite; HANDLE hStdErrRead, hStdErrWrite; // 创建匿名管道用于读取输出 CreatePipe(&hStdOutRead, &hStdOutWrite, &sa, 0); CreatePipe(&hStdErrRead, &hStdErrWrite, &sa, 0); STARTUPINFO si = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pi = { 0 }; si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; si.wShowWindow = SW_HIDE; si.hStdOutput = hStdOutWrite; // 子进程的输出写到我们的管道写端 si.hStdError = hStdErrWrite; // 如果不需输入,可以si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); TCHAR writableCmdLine[MAX_PATH]; _tcscpy_s(writableCmdLine, cmdLine); BOOL bSuccess = CreateProcess( NULL, writableCmdLine, NULL, NULL, TRUE, // 必须为TRUE,子进程才能继承我们提供的句柄 CREATE_NO_WINDOW, NULL, NULL, &si, &pi ); CloseHandle(hStdOutWrite); // 在父进程中立即关闭写端,否则读管道会一直等待 CloseHandle(hStdErrWrite); if (bSuccess) { // 从hStdOutRead和hStdErrRead管道句柄中读取数据... char buffer[4096]; DWORD bytesRead; while (ReadFile(hStdOutRead, buffer, sizeof(buffer)-1, &bytesRead, NULL) && bytesRead > 0) { buffer[bytesRead] = '\0'; printf("Output: %s", buffer); // 处理输出 } WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } CloseHandle(hStdOutRead); CloseHandle(hStdErrRead); }

重要提示:管道重定向是一个复杂的话题,涉及到句柄继承、缓冲区阻塞等问题。上面的示例是一个简化版。在实际应用中,你需要考虑异步读取、超时处理、避免死锁(比如子进程等待输入而父进程在等待输出)等情况。一个常见的坑是忘记关闭父进程中不用的管道写端,导致读端永远等不到EOF。

5. 方法三:移花接木——使用WinExec或ShellExecute

除了CreateProcess,Windows API 还提供了WinExecShellExecute这类更简单的进程启动函数。它们内部也是调用CreateProcess,但封装了更多细节。

5.1 使用 WinExec

WinExec是一个很老的API,简单但功能有限,不适合需要精细控制(如重定向IO)的场景。它可以通过一个nCmdShow参数来控制窗口显示。

// 运行并隐藏窗口(注意:对于控制台程序,可能仍会闪一下) WinExec("notepad.exe", SW_HIDE); // 最小化运行 WinExec("notepad.exe", SW_SHOWMINIMIZED);

缺点WinExec无法像CreateProcess那样使用CREATE_NO_WINDOW标志,对于控制台程序,仅靠SW_HIDE可能无法完全避免闪烁。且它无法获取进程句柄进行等待或终止操作。

5.2 使用 ShellExecute

ShellExecuteShellExecuteEx功能更强大,可以关联文件类型、提升权限等。它也可以通过nShowCmd参数控制窗口显示。

ShellExecute(NULL, _T("open"), _T("cmd.exe"), _T("/c dir"), NULL, SW_HIDE);

重要区别ShellExecute主要用于“打开”文档或程序,它依赖于Shell。对于控制台程序的隐藏,其效果与WinExec类似,也可能存在闪烁问题。ShellExecuteEx功能更强,可以获取进程句柄。

经验之谈WinExecShellExecute在快速原型或简单脚本中很方便,但对于需要稳定、可靠、无闪烁地隐藏控制台窗口的生产级代码,CreateProcess配合CREATE_NO_WINDOW是唯一推荐的选择。前两者更像是“请求Shell打开某个东西”,而后者是“直接创建一个新进程”,控制粒度完全不同。

6. 方法四:李代桃僵——编写纯Win32服务或后台程序

如果你的程序本身就是一个需要长期运行、无需用户交互的后台程序(例如Windows服务),那么它根本就不应该有窗口。这种情况下,隐藏控制台窗口是基本要求。

6.1 Windows服务程序框架

一个最简单的Windows服务程序,其mainWinMain函数通常只是设置一个服务表(Service Table)并启动服务控制分发器。

#include <windows.h> SERVICE_STATUS g_ServiceStatus = {0}; SERVICE_STATUS_HANDLE g_StatusHandle = NULL; void WINAPI ServiceCtrlHandler(DWORD dwControl) { // 处理控制请求(如停止、暂停) if (dwControl == SERVICE_CONTROL_STOP) { g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING; SetServiceStatus(g_StatusHandle, &g_ServiceStatus); // 执行清理工作... g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(g_StatusHandle, &g_ServiceStatus); } } void WINAPI ServiceMain(DWORD argc, LPTSTR *argv) { g_StatusHandle = RegisterServiceCtrlHandler(TEXT("MyService"), ServiceCtrlHandler); g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; SetServiceStatus(g_StatusHandle, &g_ServiceStatus); // 这里是服务的主循环,执行你的后台逻辑 while (g_ServiceStatus.dwCurrentState == SERVICE_RUNNING) { Sleep(1000); // 模拟工作 } } int main() { SERVICE_TABLE_ENTRY ServiceTable[] = { { TEXT("MyService"), ServiceMain }, { NULL, NULL } }; StartServiceCtrlDispatcher(ServiceTable); return 0; }

这样的程序编译链接为Windows子系统后,运行时不会有任何窗口。它由服务控制管理器(SCM)启动和管理。

6.2 控制台程序作为后台任务运行

即使不是标准的服务,一个控制台程序也可以通过以下方式“后台化”运行:

  1. 修改为Windows子系统(方法一)。
  2. 将所有输出从printf/cout改为日志文件或系统事件日志。
  3. 如果需要处理消息(如定时器、通信),可以创建一个隐藏窗口或使用窗口消息泵的替代方案(如MsgWaitForMultipleObjects)。

注意事项:编写服务程序比普通程序复杂,需要考虑会话隔离、权限、安装卸载、与SCM的通信、故障恢复等。在决定将程序改为服务前,要评估其必要性。

7. 常见问题与排查技巧实录

在实际开发中,即使知道了方法,还是会遇到各种稀奇古怪的问题。下面是我总结的一些典型场景和解决方案。

7.1 问题:窗口仍然闪烁一下

  • 可能原因1:只使用了SW_HIDE,没有使用CREATE_NO_WINDOW标志。
    • 解决:在CreateProcessdwCreationFlags参数中务必加上CREATE_NO_WINDOW
  • 可能原因2:被调用的命令行程序(如某些批处理脚本或旧工具)自身会创建窗口。
    • 解决:这种情况父进程难以控制。可以尝试用cmd /c来包装命令,并对cmd进程使用隐藏标志,但并非百分百有效。终极方案是寻找或编写替代的命令行工具。

7.2 问题:程序编译通过,但运行时报错或行为异常

  • 可能原因1:入口点冲突。将控制台项目改为Windows项目后,链接器找不到WinMain
    • 排查:检查链接错误。如果错误是_WinMain@16 not found,说明你没有提供WinMain函数。
    • 解决
      1. 提供WinMain函数。
      2. 或者在链接器 -> 高级 -> 入口点中手动指定为mainCRTStartup(如果你坚持用main)。
  • 可能原因2:使用了#pragma comment指令,但项目属性中的链接器设置覆盖了它。
    • 解决:指令的优先级通常低于项目属性设置。检查项目属性中的子系统设置,确保没有冲突。最稳妥的方法是直接在项目属性中设置。

7.3 问题:需要隐藏窗口,但又想看到程序的输出进行调试

这是一个典型的开发期需求。

  • 解决方案:使用条件编译。
    #ifdef _DEBUG // 调试模式:显示窗口,或输出到调试器 si.wShowWindow = SW_SHOWMINIMIZED; // 或者 SW_SHOW DWORD flags = 0; #else // 发布模式:彻底隐藏 si.wShowWindow = SW_HIDE; DWORD flags = CREATE_NO_WINDOW; #endif BOOL bSuccess = CreateProcess(..., flags, ...);
    或者,在调试时将所有输出重定向到一个文件,发布时再改为隐藏。

7.4 问题:隐藏窗口后,程序似乎“卡住”或无法结束

  • 可能原因:被调用的子进程在等待输入(例如,某些命令需要确认“按任意键继续”),而输入句柄没有被正确重定向或关闭。
  • 排查与解决
    1. 检查命令本身是否需要交互。尝试在普通cmd中运行该命令,看是否需要输入。
    2. 如果不需要交互,确保在STARTUPINFO中为hStdInput提供了一个有效的句柄(例如,一个空的管道读端或NUL设备的句柄),防止子进程等待永远不会到来的输入。
    3. 如果子进程是批处理(.bat),cmd /c执行完毕后可能会等待。可以尝试在命令末尾加上& exit

7.5 关于“运行bat+命令行+隐藏窗口”热词的特别说明

很多开发者希望隐藏运行批处理文件(.bat或.cmd)。这里有一个关键点:你隐藏的是cmd.exe进程的窗口,而不是批处理文件本身。

// 正确做法:隐藏的是cmd.exe RunCommandHidden(_T("cmd /c \"your_batch_file.bat && exit\""));

/c参数告诉cmd执行完命令后终止。&& exit是为了确保即使批处理中最后一条命令出错,cmd也会退出。双引号用于处理路径中的空格。

8. 高级话题与扩展思考

掌握了基本方法后,我们可以探讨一些更深入的应用场景和优化。

8.1 如何隐藏由system()函数调用的命令窗口?

system()函数是C标准库函数,它内部会调用cmd.exe。要隐藏它的窗口,直接修改system()的行为很困难。通常的解决方案是弃用system(),改用CreateProcess。这是最佳实践,因为CreateProcess提供了更精细的控制。

如果你因为历史代码等原因必须使用system(),可以尝试一个非常规的、不保证跨平台和稳定性的方法:修改标准库的实现或使用平台相关的hook技术,但这超出了普通应用开发的范畴,不推荐。

8.2 隐藏自身程序的控制台窗口

如果你的程序本身是一个控制台程序,想在运行时隐藏自己的窗口,这要困难得多。因为窗口在程序入口点之前就已经由操作系统创建好了。

  • 一种方法:编写一个极小的“启动器”程序(Windows子系统),由它来调用你的主程序(控制台子系统)并隐藏窗口。这本质上又回到了方法二。
  • 另一种偏门技巧:在main函数开始处使用ShowWindow(GetConsoleWindow(), SW_HIDE);。但这只是隐藏了已经存在的窗口,在快速机器上可能仍会看到一闪而过。且GetConsoleWindow()函数在早期Windows版本中可能不可用。

8.3 与其他技术栈的交互

从热词中可以看到,这个问题不仅限于纯VC++。例如:

  • Flutter/Android开发:在打包或构建过程中,可能会调用原生的VC++编译工具链(如MSBuild),这些调用可能会弹出控制台窗口。通常需要在构建脚本(如Gradle, CMake)中配置相应的参数来静默执行。
  • Python/Java等脚本或跨平台语言:它们调用系统命令时(如os.system,Runtime.exec),也可能面临同样问题。在Windows上,其底层最终也是调用CreateProcess或类似机制。高级语言通常提供了设置窗口样式的参数,例如Python的subprocess模块可以设置creationflags=subprocess.CREATE_NO_WINDOW

隐藏DOS窗口虽然是一个具体的功能点,但它贯穿了Windows程序设计的多个基础概念。理解子系统、入口点、进程创建和窗口管理之间的关系,不仅能解决眼前的问题,更能加深对Windows平台程序运行机制的理解。在实际项目中,根据需求选择最合适、最稳定的方法,并做好错误处理和日志记录,才能打造出真正专业的软件。

http://www.cnnetsun.cn/news/3947513.html

相关文章:

  • 【单片机】利用双缓冲机制保护全局变量不受中断影响以及环形队列
  • 罗源县建设局网站如何助力民生与建设:全方位解读罗源县建设局网站
  • UI Auto Monkey自动化测试:原理、架构与Python实战
  • GetQzonehistory终极指南:完整备份QQ空间说说的免费解决方案
  • 5分钟掌握Windows平台消息防撤回技术:RevokeMsgPatcher完整解决方案
  • 本地DNA分析实战:使用SuperDNA命令行工具处理基因原始数据
  • LangChain框架解析:从核心概念到RAG应用实战
  • AI论文写作工具全攻略:从文献检索到自动引用
  • Raw Accel 终极指南:Windows鼠标加速驱动完整教程
  • 用Wand-Enhancer方案彻底突破WeMod专业版限制:3步实现永久免费解锁
  • FlicFlac音频格式转换工具:5分钟快速上手的完整指南
  • 虚拟机网站建设全攻略:从零基础入门到高效运维的真心话
  • React嵌套属性编辑器实现与优化指南
  • 免费下载B站大会员4K视频:Python开源工具完整指南
  • AI Agent 面试题 465:Agent的目标达成度评估和进度追踪机制
  • AMD锐龙处理器调试工具:SMUDebugTool完全使用指南
  • 终极Windows和Office激活解决方案:KMS_VL_ALL_AIO智能激活工具完整指南
  • 算法面试——二分查找:旋转数组、二维矩阵、边界
  • 如何快速获取网易云和QQ音乐的LRC歌词?这个开源工具让你告别手动搜索烦恼
  • 如何去除Gemini复制的多余符号?AI导出鸭实测!手慢无!
  • 一个没加 volatile 的单例,让 1% 的请求拿到了半初始化的对象
  • 广西住房和城乡建设厅网站_官方办事入口与最新政策查询全指南
  • OpenClaw Windows部署 保姆级操作教程,全程可视化无需命令行配置
  • Steam创意工坊免费下载终极指南:跨平台开源工具WorkshopDL深度解析
  • AI - Java之Spring AI Alibaba
  • Self-Correcting Large Language Models: Generation vs. Multiple Choice
  • 如何高效清理重复照片:AntiDupl智能图片去重工具完整指南
  • 从部署到工程化:MiniMax H3视频生成模型本地应用深度解析
  • 网站规划与建设进度如何把控:从0到1的实战复盘与避坑指南
  • 5个简单步骤:FanControl终极风扇控制配置指南