Windows Server 2019下MFC应用跨进程用窗口句柄通信时GetDC返回NULL如何解决
问题描述
有两个MFC应用,第一个应用会启动24个第二个应用的实例,同时将自身窗口句柄以字符串参数的形式传递给第二个应用。
父进程启动子进程代码
void DispatchOnTimer(HWND hwnd, UINT id) /*************************************************************/ /* * Routine called every 5 seconds to see if any new images. Only * spawn sub dispatchers on first call. */ { char TimeString[40], OldImage[_MAX_DIR], TempBuffer[81]; int Count; long FileHandle; struct _finddata_t FileInfo; STARTUPINFO si; PROCESS_INFORMATION pi; if (Startup) { Startup = 0; /* cleanup all old images */ TextMessage(hwnd, "Searching for new images..."); strcpy(OldImage, FAX_RECV_PATH); strcat(OldImage, "*.tif"); if ((FileHandle = _findfirst(OldImage, &FileInfo)) != -1) { do { if (FileInfo.size <= MIN_TIFF_SIZE) { /* if just an 8 byte errored file delete it */ TextMessage(hwnd, "Image found, deleting..."); sprintf(OldImage, "%s%s", FAX_RECV_PATH, FileInfo.name); _unlink(OldImage); } } /* while files still found */ while (_findnext(FileHandle, &FileInfo) != -1); _findclose(FileHandle); } /* if first file found */ /* Winexec fax controllers */ TextMessage(hwnd, "Initializing fax cards"); for (Count = 0; Count < Server.TotalPorts; ++Count) { memset(TempBuffer, 0x00, 81); sprintf(TempBuffer, "fax_recv -a%d -b%d -c%c -d%d -e%s", hwnd, Count, Server.Id, Server.Carrier, Server.Prefix); ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); // 09/09/2014 - BT - Replaced "WinExec()" with "CreateProcess()". //if (WinExec(TempBuffer, SW_SHOW) < 32) if(!CreateProcess(NULL, // Module name will be first on the command line TempBuffer, // Command line parameters NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE 0, // No creation flags NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi)) // Pointer to PROCESS_INFORMATION structure { memset(TempBuffer, 0x00, 81); sprintf(TempBuffer, "Error spawning line controller %d!", Count); MessageBox(hwnd, TempBuffer, "ERROR", MB_OK); } else // Success! { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } } /* loop to fire up board executables */ } /* if first call to timer routine */ /* Display the time in the window */ GetFormatTime(TimeString); TextMessage(hwnd, TimeString); ErrorMessage(hwnd, " ", 0); } /* DispatchOnTimer() */
子进程绘制父窗口代码
void CFax_recvView::TextMessage(char acText[], int iXPosition, int iYPosition) { CNewCommandLineInfo cmdInfo; pMainWnd = CWnd::FromHandle((HWND)cmdInfo.GetMainWindow()); CDC *dc = pMainWnd->GetDC(); dc->SetTextColor(RGB(0, 0, 0)); /* black */ dc->TextOut(iXPosition, iYPosition, " ", 25); dc->TextOut(iXPosition, iYPosition, acText, strlen(acText)); dc->DeleteDC(); } // "TextMessage()"
其中cmdInfo.GetMainWindow()调用会返回供第二个应用使用的第一个应用的窗口句柄。这套逻辑已完美运行20年,迁移到Windows Server 2019后无法正常工作,pMainWnd->GetDC()调用现在返回NULL。
解决方案
- 优先替换跨进程直接绘图逻辑,改用消息通知实现
跨进程操作另一个进程的GDI资源从来都不是微软官方支持的用法,旧版Windows出于兼容目的没有限制,Windows Server 2019对系统安全和资源隔离做了加强,直接禁止了这类操作。
调整方案:- 父进程注册自定义窗口消息,比如
#define WM_UPDATE_UI_TEXT (WM_USER + 100) - 子进程不再直接获取父窗口DC绘图,而是通过
SendMessage将需要绘制的文本、坐标等参数传递给父窗口 - 父进程在自身的消息处理函数中完成绘图操作,所有GDI操作都在同一进程内完成,完全规避跨进程权限和资源隔离问题。
- 父进程注册自定义窗口消息,比如
- 临时兼容调整(适合不想重构核心逻辑的场景)
- 修正DC释放逻辑:原有代码中
dc->DeleteDC()是完全错误的用法,通过GetDC()获取的设备上下文必须调用ReleaseDC(pMainWnd->GetSafeHwnd(), dc)释放,错误的释放方式会导致GDI资源泄漏,Windows Server 2019对GDI资源限制更严格,泄漏很快会导致GetDC()返回NULL。 - 绕过MFC包装直接调用系统API:先调用
IsWindow((HWND)cmdInfo.GetMainWindow())确认传入的窗口句柄有效,再直接调用HDC hdc = ::GetDC((HWND)cmdInfo.GetMainWindow())获取DC,避免MFC临时对象跨进程的异常问题。 - 解除UIPI权限限制:确认父子进程运行在同一个用户会话下,且UAC权限等级完全一致(比如都以普通用户运行,或都以管理员身份运行),避免低权限进程被禁止操作高权限进程的窗口资源。如果父进程是作为Windows服务运行的,必须关闭服务的"允许服务与桌面交互"选项,将父进程也改为用户会话下运行的普通应用,新版Windows中会话0的窗口是完全隔离的,用户态进程无法访问。
- 调整GDI资源上限:如果确认是GDI资源泄漏导致的问题,可暂时通过修改注册表
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota项,调高单进程GDI句柄上限缓解问题。
- 修正DC释放逻辑:原有代码中
内容的提问来源于stack exchange,提问作者mschauer72
相关产品推荐
相关产品推荐

