C++检测多显示器并把窗口显示在不同显示器上(完整源码)

时间:2024-03-25 19:41:08

初级代码游戏的专栏介绍与文章目录-CSDN博客

        早先大部分应用都不考虑多显示的问题。

        如果是多窗口应用,子窗口不会被限制在父窗口里面的,可以轻松把窗口拖到不同的显示器上。

        但是很多流行的界面都是一个全屏主窗口,然后其他窗口都只能在主窗口范围内,这种程序就没法自动适应多显示器了。

        但是现在专门针对多显示器的需求增多了,比如视频监控类的应用,用户会很喜欢使用多显示器,像这样就需要程序支持多显示器,并且最好是自动使用多显示器而不需要用户拖放窗口。

目录

一、多显示器概要

二、获取显示器数量和坐标

三、显示窗口到副显示器

四、相关技术点

4.1 EnumDisplayDevices

4.2  EnumDisplaySettingsEx

4.3 GetSystemMetrics(SM_CMONITORS)

4.4 MFC的MoveWindow


一、多显示器概要

        多显示器共享同一个屏幕坐标空间,主显示器左上角为(0,0),其余显示器排列在其它位置,坐标可能是负值。具体如何取决于显示设置,可以用鼠标拖放显示器来控制显示器的位置关系。

        多个显示器的坐标可能会有间隔!原因是窗口最大化时阴影和边框位于显示器外面,但是又不能出现在别的显示器上,所以显示器之间有间隔。这些问题可以通过获取窗口坐标来验证。

二、获取显示器数量和坐标

        获取所有显示器信息的代码:

RECT m_ScrRect[10];
int GetScreenRect()
{
	int count = 0;
	for (int ScreenNo = 0;true; ++ScreenNo)
	{
		BOOL flag;
		DISPLAY_DEVICE dd;
		ZeroMemory(&dd, sizeof(dd));
		dd.cb = sizeof(dd);
		
		//枚举显示器,获取后面要用的名字,注意这会返回系统所能支持的所有显示器,ScreenNo从0开始,直到返回FALSE
		flag = EnumDisplayDevices(NULL, ScreenNo, &dd, EDD_GET_DEVICE_INTERFACE_NAME);
		if (!flag)
		{
			break;
		}
		
		DEVMODE dm;
		ZeroMemory(&dm, sizeof(dm));
		dm.dmSize = sizeof(dm);
		//返回当前设置,如果失败表明显示器不在线
		flag = EnumDisplaySettingsEx(dd.DeviceName, ENUM_CURRENT_SETTINGS, &dm, 0);
		if (!flag)
		{
			continue;
		}

		m_ScrRect[count].left = dm.dmPosition.x;//如果副显示器在左边,则这个值是负的
		m_ScrRect[count].top = dm.dmPosition.y;
		m_ScrRect[count].right = m_ScrRect[count].left + dm.dmPelsWidth - 1;
		m_ScrRect[count].bottom = m_ScrRect[count].top + dm.dmPelsHeight - 1;
		++count;
		thelog << dd.DeviceName << " (dmBitsPerPel "<<dm.dmBitsPerPel <<" dmLogPixels" <<dm.dmLogPixels << ") " 
			<< dm.dmPosition.x << " " << dm.dmPosition.y << " " << dm.dmPelsWidth << " " << dm.dmPelsHeight << endi;
	}
	thelog << "检索到显示器 " << count << endi;
	return count;
}

        注意这段代码假设显示器最多有10个。thelog是日志,endi表示一般信息,可以用cout和endl代替。后面的测试代码会用这个函数获取多显示器信息。

        相关技术点:

  • EnumDisplayDevices 枚举系统所有的显示器,这是系统所支持的最大数量,每个都预先分配了名字
  • EnumDisplayDevices 获取显示器信息,如果显示器不存在就会返回失败,但是存在的显示器并不是连续的,所以每个都要检查

三、显示窗口到副显示器

        修改显示about对话框的代码(对话框项目),显示在副显示器上:

//这里改成了非模态对话框,用来验证多显示器支持,里面记录的奇怪的问题都是因为项目设置没有设置“每个监视器高DPI识别”导致的
CAboutDlg dlgAbout;
void CTestDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
		//奇怪的问题,在副屏上如果已经是最大化,再次打开就会消失,但是主屏不会
		//计算得到的RECT和实测的不一样,实测的x似乎大一倍
		//由于尺寸问题,最大化可以在副屏显示,但是要先隐藏,不然再次打开就消失,而且再也不会出现
		if (!dlgAbout.GetSafeHwnd())
		{
			dlgAbout.Create(IDD_ABOUTBOX, this);
			
		}
		else { thelog << "无模式对话框已存在" << ende; }
		int monitors = GetSystemMetrics(SM_CMONITORS);//这个直接返回显示器个数
		if (GetScreenRect() != monitors)
		{
			thelog << "检索显示器信息出错" << ende;
		}
		else
		{
			RECT* pMonitorRect = &m_ScrRect[monitors - 1];//取最后一个显示器
			RECT rect;
			dlgAbout.GetWindowRect(&rect);
			rect.right = pMonitorRect->left + rect.right - rect.left;
			rect.bottom = pMonitorRect->top + rect.bottom - rect.top;
			rect.left = pMonitorRect->left;
			rect.top = pMonitorRect->top;
			thelog << "目标 " << rect.left << " " << rect.top << " " << rect.right << " " << rect.bottom << endi;
			dlgAbout.MoveWindow(&rect);
			//很奇怪,这样就不显示,最大化就显示SW_SHOWMAXIMIZED
			if (0 == dlgAbout.ShowWindow(SW_SHOW))thelog << "ShowWindow成功" << endi;
			else thelog << "ShowWindow已经是显示的" << ende;
			RECT curr;
			dlgAbout.GetWindowRect(&curr);
			thelog << "实际" << curr.left << " " << curr.top << " " << curr.right << " " << curr.bottom << endi;
		}
		if (0==dlgAbout.ShowWindow(SW_SHOW))thelog << "ShowWindow成功" << endi;
		else thelog << "ShowWindow已经是显示的" << ende;
		
	}
	else
	{
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

        这个代码自动把窗口显示在副显示器上,并测试了放大缩小等窗口操作。

        自动显示到副显示器很简单,就是把窗口移动到副显示器的坐标范围内而已。 

        以上代码是win10+VS2017环境的。

四、相关技术点

4.1 EnumDisplayDevices

winuser.h
User32.dll/User32.lib

BOOL EnumDisplayDevicesA(
  [in]  LPCSTR           lpDevice,
  [in]  DWORD            iDevNum,
  [out] PDISPLAY_DEVICEA lpDisplayDevice,
  [in]  DWORD            dwFlags
);

        这个函数列举所有设备,知道设备索引超出最大值。所以调用要这个函数需要一个循环,并不断递增iDevNum,直到调用失败。但每个成功返回的设备并不一定是在线的,看起来就是系统预先准备了所支持的最大数量的入口。

4.2  EnumDisplaySettingsEx

Winuser.h
User32.dll/User32.lib

BOOL EnumDisplaySettingsExA(
  [in]  LPCSTR   lpszDeviceName,
  [in]  DWORD    iModeNum,
  [out] DEVMODEA *lpDevMode,
  [in]  DWORD    dwFlags
);

        这个函数获取具体一个显示器的信息,设备名来自之前的枚举过程,显示器不在线就会失败。系统分配显示器索引并不是连续的,所以要逐个检查之前用EnumDisplayDevices获得的有效设备名。

4.3 GetSystemMetrics(SM_CMONITORS)

Winuser.h
User32.dll/User32.lib

int GetSystemMetrics(
  [in] int nIndex
);

        这个调用直接获取显示器个数,但是不能获取显示器信息。

        参数nIndex指示不同的功能,比如SM_CMOUSEBUTTONS表示鼠标按钮数量。

4.4 MFC的MoveWindow

        这个函数用来改变窗口位置。

(这里是文档结束)