这几天研究了一下Windows系统的多显示器模式的编程,实现了Windows下支持10显示器模式的通用com组件,这里做一个整理和回顾,希望能对再这方面开发的兄弟们有些启发和帮助:
 
(一) Windows系统下的多显示器模式的原理
 
    Microsoft新的操作系统(Windows 98\Windows 2000\Windows XP)内置了对多监视器的支持,即用户可以在一台计算机上安装多个显示卡并接上多个显示器,然后把这些 显示器的显示区域组织成一个大的虚拟的Windows桌面。每一个显示区域的底部都有系统任务栏,我们可以在任何一个显示区域内增加桌面快捷方式,这样就 可以在第一个显示区域上用Visual C++编程,同时在第二个显示区域上打开Internet Explorer上网——再也不用进行麻烦的切换了。
   多显示器模式的原理实际上很简单,主要还是要靠操作系统的支持,比如WinXP就支持10个显示器,本文所使用的调试和开发环境都是以WinXP为主,其余的原理都相同慢慢调试就行了.
 
Windows提供的多显示器模式主要有以下三个功能:

  1.更大的Windows桌面:在多显示器模式下,可以把多个显示器的显示区域结合在一起来显示 Windows桌面,不管这些显示器的尺寸、物理位置、分辨率和刷新频率是否相同。当我们运行一个应用程序时,程序的主窗口可以位于任何一个显示器的显示 区域内,也可以跨多个显示区域。我们也可以把一个程序的窗口从一个显示区域移到另一个显示区域中。

  2. 屏幕复制或远程显示:我们可以让两个显示器显示相同的内容。在进行培训或者向众人进行演示时,这个特点是很有用的。利用这个特性,技术支持人员还可以对应用程序进行远程监视和调试。

  3.多重独立显示:在以上的两种模式下,所有的显示区域都是Windows虚拟桌面的一部分,但 是在多重独立显示模式下,应用程序访问的显示器并不属于Windows虚拟桌面。假设系统的第二个显示器是一个高分辨率的大尺寸显示器,我们可以把它用做 CAD应用程序的专用显示。通过在CAD应用程序中调用新的Windows API,我们可以借助GDI在上面画图。独立显示器的显示区域没有桌面上的任何对象(任务栏和快捷方式),它与Windows桌面是独立的。这可以避免 Windows桌面对应用程序输出的任何干扰,我们也不用担心会在无意中把其它的窗口拽到独立显示的显示区域中,这种方式就好像为应用程序提供了一个专用 的显示器。

(二)理解虚拟桌面(Virtual Desktop)及其坐标
 
既然是要对多显示器模式进行编程和开发,那么我们就要首先理解Windows的虚拟桌面(Virtual Desktop)及其坐标了.这是我们编程开发的基础,理解了一切就很顺利了,几乎没有什么难度.

  在单显示器系统中,实际Windows桌面的形状和大小与显示器是相同的。在多显示器模式下,每一个显示器实际上是一个大虚拟桌面的一个“子视窗”。

  我们可以通过控制面板中的显示器属性对每一个显示器的显示区域的大小(分辨率)和相对位置进行调 整,所有这些显示区域互相连接但并不重叠。图一中的显示器1是主显示器,主显示器的作用是确定虚拟桌面的坐标。不管主显示器的位置如何,它的显示区域的左 上角的坐标定为虚拟坐标的零点(0,0),右下角的坐标是(X-1,Y-1)(假设主显示器的分辨率为X×Y),其余显示区域的坐标由它和主显示器的相对 位置决定。通常虚拟桌面中显示区域的相对位置和实际显示器的物理相对位置是相同的。因为所有显示区域必须相连,因此可以用一个包含所有显示区域的最小矩形 来表示虚拟桌面的大小。图一中的矩形边界代表了虚拟桌面的范围。

  因为虚拟桌面中的坐标系统必须是连续的,因此第二个显示区域的坐标是主显示器的显示区域的继续。 假设两个显示器都使用1024×768的分辨率,并且第二个显示器位于第一个显示器(主显示器)的正右方,则第二个显示区域的坐标是从(1024,0)到 (2047,767)。

  但是并不是所有的显示区域都具有相同的分辨率,而且这些显示区域也不一定是底边对齐的。就像图一 中显示的那样,你真正能看到的有效显示区域是红色+兰色+紫色的不规则区域,而黄色区域虽然也属于虚拟桌面的一部分,但它不属于任何一个显示区域,这部分 也叫做无效区域。如图一中所示,假设显示器1的分辨率是1024×768,显示器2的分辨率为800×600,显示器3的分辨率为640×480。零点的 位置如图中所示,显示器1的坐标为(0,0)到(1023,767),显示器2的坐标为(-800,168)到(-1,767),显示器3的坐标是 (1024,0)到(1663,479)。而(-800,0)到(-1,167)以及(1024,480)到(1663,767)这两块无效区域是不能显 示任何信息的,系统不会允许用户把鼠标移动到这两个区域。需要注意的是无效区域是包括在虚拟桌面中的,因此图一中的虚拟桌面的大小是从(-800,0)到 (1663,767)。

    我在编程开发的过程中就使用了2个显示器,一个是自己的笔记本,分辨率为1024×768作为主显示器,另外一个由于比较懒,直接找了一个小巧的NEC12寸屏幕的小黑白显示器,不是为了别的搬着方便啊,这个NEC黑白支持分辨率800×600,强吧.

如下图我是直接设置了扩展桌面,两个显示器就都可以使用了

在这里要注意主显示器和副显示器的区别,其实主显示器和副显示器你是可以进行任意调整的.

(三)系统支持编程开发的API

  Microsoft为支持多显示器模式提供了一些新的API调用,下面具体介绍它们的功能:

  1.HMONITOR MonitorFromPoint(POINT pt,DWORD dwFlags)

  MonitorFromPoint返回包含特定点(pt)的一个显示器句柄。如果pt不属于任何 一个显示器,返回的显示器句柄由dwFlags标志决定:MONITOR_DEFAULTTONULL时返回 NULL,MONITOR_DEFAULTTOPRIMARY时返回代表主显示器的HMONITOR句 柄,MONITOR_DEFAULTTONEAREST时返回最靠近pt点的显示器的HMONITOR句柄。 2.HMONITOR MonitorFromRect(LPCRECT lprc,DWORD dwFlags)

  MonitorFromRect返回包含lprc代表的矩形的显示器句柄;如果包含此矩形的显示区域不止一个,则返回包含矩形最大部分的显示器句柄;如果矩形不属于任何一个显示区域,返回的句柄由dwFlags决定,规则与MonitorFromPoint相同。

  3. HMONITOR MonitorFromWindow(HWND hwnd,DWORD dwFlags)

  与MonitorFromRect类似,但输入是一个代表窗口的句柄hwnd而不是指向矩形的指针。

  4. BOOL GetMonitorInfo(HMONITOR hMonitor,LPMONITORINFO lpmi)

  GetMonitorInfo返回由hMonitor代表的显示器的有关信息,这些信息存储在指 向MONITORINFO结构的指针——lpmi中。这些信息包括用RECT结构表示的显示器的显示区域的大小(如果这个显示器不是主显示器,RECT的 坐标可能为负数),以及用RECT结构表示的显示器的工作区域的大小,工作区域是显示区域中除去系统任务栏和应用程序快捷方式栏所剩下的区域,还能够判断 此显示器是否为主显示器,并返回一个标志。

  5.BOOL EnumDisplayMonitors(HDC hdc,LPCRECT lprcClip,MONITORENUMPROC lpfnEnum,LPARAM dwData)

  hdc是一个代表显示设备环境的句柄,lprcClip是指向一个矩形区域的指针。把这个矩形区 域和设备环境中的可见区域取交集,得到的区域可能分布在多个显示器的显示区域中,EnumDisplayMonitors对每一个包含交集的显示区域调用 一次MonitorEnumProc类型的函数。DwData为传递给MonitorEnumProc函数的数据。

  6.BOOL CALLBACK MonitorEnumProc(HMONITOR hmonitor,HDC hdcMonitor,LPRC lprcMonitor, DWORD dwData)

  MonitorEnumProc是一个被EnumDisplayMonitors函数调用的回调函数,它的内容可以由用户自定义。利用这两个函数,用户在进行跨多个显示器的显示时就可以利用每一个显示器的不同的显示特性。

  当然,并不是所有画图程序都必须调用这两个函数,这时你假设所有的显示器都使用同样颜色的分辨率。

  7.EnumDisplayDevices(LPVOID lpReserved,int iDeviceNum,DISPLAY_DEVICE×pDisplayDevice,DWORD dwFlags)

  EnumDisplayDevices列出系统中某个显示设备(以iDeviceNum为序号) 的信息。与GetMonitorInfo相比,GetMonitorInfo对应的显示器必须是Windows虚拟桌面的一部分,而 EnumDisplayDevices可以列出包括处于独立显示模式下的系统所安装的所有显示器的信息。它返回的信息储存在DISPLAY_DEVICE 结构中,包括显示设备名称、对显示设备的描述和显示设备的状态。

  此外,一些原有的API调用如SystemParametersInfo和 GetSystemMetrics也加入了对多显示器模式的支持。比如调用GetSystemMetrics时,如果用 SM_XVIRTUALSCREEN、SM_YVIRTUALSCREEN、SM_CXVIRTUALSCREEN和 SM_CYVIRTUALSCREEN,得到的是虚拟桌面左上角的坐标和整个的长度和宽度。

  我们在编程时特别要注意坐标的变化:首先单显示器下负坐标或大于SM_CXSCREEN和 SM_CYSCREEN部分的窗口将被隐藏,而在多显示器模式下这些都是合法的。其次在确定应用程序窗口和对话框的位置时,要选择正确的显示器和正确的全 局坐标(虚拟桌面坐标)。最后,在恢复原来存储的窗口之前,要检查一下这些窗口坐标的有效性。

     这些都可以在微软的MSDN上去查出来,需要仔细的看一看,每个API都亲自试一试.

    大家可以参考MSND的一篇文章"How to Exploit Multiple Monitor Support in Memphis and Windows NT 5.0",说的很详细.

 

(四)实现多屏幕编程的组件设计
 
这个组件参考了网上的许多资料,这里先向那些无私的同行表示感谢,我做的工作只是将他们的成果进行了系统化的整理......
 
组件的设计流程如下:

 

(1).初始化程序

Syntax:: MScreenInfo();

Description : 部件构造函数,初始化部件,获取系统屏幕信息,设置部件属性。

 


 

 

(2). 获取指定屏幕的宽度

Syntax: Short GetScreenWidth( Short ScreenNo) ;

Input : ScreenNo -- 指定屏幕的序号,0 -- m_monitorNum-1;

Return: Screen Width in Pixel;

Decription: 获取ScreenNo指定屏幕的宽度。

 


 

 

(3). 获取指定屏幕的高度

Syntax: Short GetScreenHeight( Short ScreenNo) ;

Input : ScreenNo -- 指定屏幕的序号,0 -- m_monitorNum-1;

Return: Screen Height in Pixel;

Decription: 获取ScreenNo指定屏幕的高度。

程序流程图:与图2相同,只是最后一步返回dm.dmPelsHeight.

 

(4). 获取指定屏幕的坐标原点-left

Syntax: Short GetScreenLeft( Short ScreenNo) ;

Input : ScreenNo -- 指定屏幕的序号,0 -- m_monitorNum-1;

Return: Screen Left in Pixel;

Decription: 获取ScreenNo指定屏幕的坐标原点-left

程序流程图:与图2相同,只是最后一步返回dm.dmPosition.x.

 

(5). 获取指定屏幕的坐标原点-top

Syntax: Short GetScreenLeft( Short ScreenNo) ;

Input : ScreenNo -- 指定屏幕的序号,0 -- m_monitorNum-1;

Return: Screen Top in Pixel;

Decription: 获取ScreenNo指定屏幕的坐标原点-top

程序流程图:与图2相同,只是最后一步返回dm.dmPosition.y.

 

(6). 获取主屏幕--Primary Screen

Syntax: Short GetPrimaryScreen();

Input: Null;

Return: Primary Screen No, 0 -- m_monitorNum - 1

Description: 获取主屏幕的序号。

程序流程:依次判断那一个屏幕的原点是(0, 0).

 

 

(五)组件开发的实现和主要代码
 
1 开发环境
 
操作系统: WindowsXP   编程环境: VC 6.0
 
2 组件接口如下
 


 

3 主要代码

// 获得显示器的数量

CMScreenInfoCtrl::CMScreenInfoCtrl()
{
 InitializeIIDs(&IID_DMScreenInfo, &IID_DMScreenInfoEvents);

 // 找出显示器的总数量
 int  i;
 BOOL flag;
 DISPLAY_DEVICE dd;

 i = 0;
 flag = true;
    ZeroMemory(&dd, sizeof(dd));
    dd.cb = sizeof(dd);
 do
 {
  flag = EnumDisplayDevices(NULL, i, &dd, 0);
  if (flag) i += 1;
 } while (flag);

 m_monitorNum = i;  // 总数量
}

// 获得显示区宽度

short CMScreenInfoCtrl::GetScreenWidth(short ScreenNo)
{
 if (ScreenNo < 0 || ScreenNo >= m_monitorNum) return 0;

 BOOL flag;
 DISPLAY_DEVICE dd;

    ZeroMemory(&dd, sizeof(dd));
    dd.cb = sizeof(dd);
 flag = EnumDisplayDevices(NULL, ScreenNo, &dd, 0);

 if (!flag) return 0;

 DEVMODE dm;
 ZeroMemory(&dm, sizeof(dm));
 dm.dmSize = sizeof(dm);
 flag = EnumDisplaySettings((char*)dd.DeviceName,ENUM_CURRENT_SETTINGS, &dm);

 if (!flag) return 0;

 return (short) dm.dmPelsWidth;
}

// 设置显示区宽度

void CMScreenInfoCtrl::SetScreenWidth(short ScreenNo, short nNewValue)
{
 SetModifiedFlag();
}

// 获得显示区宽度

short CMScreenInfoCtrl::GetScreenHeight(short ScreenNo)
{
 if (ScreenNo < 0 || ScreenNo >= m_monitorNum) return 0;

 BOOL flag;
 DISPLAY_DEVICE dd;

    ZeroMemory(&dd, sizeof(dd));
    dd.cb = sizeof(dd);
 flag = EnumDisplayDevices(NULL, ScreenNo, &dd, 0);

 if (!flag) return 0;

 DEVMODE dm;
 ZeroMemory(&dm, sizeof(dm));
 dm.dmSize = sizeof(dm);
 flag = EnumDisplaySettings((char*)dd.DeviceName,ENUM_CURRENT_SETTINGS, &dm);

 if (!flag) return 0;

 return (short) dm.dmPelsHeight;
}

// 设置显示区高度

void CMScreenInfoCtrl::SetScreenHeight(short ScreenNo, short nNewValue)
{
 SetModifiedFlag();
}

// 获得显示区Y坐标

short CMScreenInfoCtrl::GetScreenTop(short ScreenNo)
{
 if (ScreenNo < 0 || ScreenNo >= m_monitorNum) return -1;

 BOOL flag;
 DISPLAY_DEVICE dd;

    ZeroMemory(&dd, sizeof(dd));
    dd.cb = sizeof(dd);
 flag = EnumDisplayDevices(NULL, ScreenNo, &dd, 0);

 if (!flag) return -1;

 DEVMODE dm;
 ZeroMemory(&dm, sizeof(dm));
 dm.dmSize = sizeof(dm);
 flag = EnumDisplaySettings((char*)dd.DeviceName,ENUM_CURRENT_SETTINGS, &dm);

 if (!flag) return -1;

 return (short) dm.dmPosition.y ;
}

// 设置显示区Y坐标

void CMScreenInfoCtrl::SetScreenTop(short ScreenNo, short nNewValue)
{
 SetModifiedFlag();
}

// 获得显示区X坐标

short CMScreenInfoCtrl::GetScreenLeft(short ScreenNo)
{
 if (ScreenNo < 0 || ScreenNo >= m_monitorNum) return -1;

 BOOL flag;
 DISPLAY_DEVICE dd;

    ZeroMemory(&dd, sizeof(dd));
    dd.cb = sizeof(dd);
 flag = EnumDisplayDevices(NULL, ScreenNo, &dd, 0);

 if (!flag) return -1;

 DEVMODE dm;
 ZeroMemory(&dm, sizeof(dm));
 dm.dmSize = sizeof(dm);
 flag = EnumDisplaySettings((char*)dd.DeviceName,ENUM_CURRENT_SETTINGS, &dm);

 if (!flag) return -1;

 return (short) dm.dmPosition.x ;
}

// 设置显示区X坐标

void CMScreenInfoCtrl::SetScreenLeft(short ScreenNo, short nNewValue)
{
 SetModifiedFlag();
}

// 获得主显示区

short CMScreenInfoCtrl::GetPrimaryScreen()
{
 // TODO: Add your property handler here
 if (m_monitorNum <= 1) return 0;

 // if the Screen Top = 0 and Left = 0, then, it's the Primary Screen
 short i;
 for (i=0; i<m_monitorNum; i++)
 {
  if (GetScreenTop(i)==0 && GetScreenLeft(i)==0) return i;
 }
 return 0;
}

// 设置主显示区

void CMScreenInfoCtrl::SetPrimaryScreen(short nNewValue)
{
 SetModifiedFlag();
}

关键的代码基本就是这些了.

(3)组件发布

直接编译成为ocx组件,取名为MutlScreen.ocx

使用regsvr32.exe注册一下就可以使用了.

(六)分屏输出组件的应用
 
我们使用最简单的VB来编写一个小程序实现
 
1 建立一个VB的工程,引用组件,建立两个form,分别为frmCtl,和frmOutScreen,我们将frmOutScreen输出到第二个屏幕上;
2 组件使用的代码如下:
 
Function initMotion()
    numScreen = frmCtl.MScreenInfo1.MonitorNum
    primaryScreen = frmCtl.MScreenInfo1.primaryScreen
   
    wScreen1 = frmCtl.MScreenInfo1.screenWidth(0)
    hScreen1 = frmCtl.MScreenInfo1.screenHeight(0)
    topScreen1 = frmCtl.MScreenInfo1.ScreenTop(0)
    leftScreen1 = frmCtl.MScreenInfo1.ScreenLeft(0)
   
    wScreen2 = frmCtl.MScreenInfo1.screenWidth(1)
    hScreen2 = frmCtl.MScreenInfo1.screenHeight(1)
    topScreen2 = frmCtl.MScreenInfo1.ScreenTop(1)
    leftScreen2 = frmCtl.MScreenInfo1.ScreenLeft(1)
   
End Function
 
3 frmOutScreen的代码
 
Private Sub Form_Load()
frmOutScreen.Left = leftScreen2 * 15 + 1
frmOutScreen.Top = 8
frmOutScreen.WindowState = 2
frmOutScreen.WindowsMediaPlayer1.Left = 0
frmOutScreen.WindowsMediaPlayer1.Top = 0
frmOutScreen.WindowsMediaPlayer1.Width = frmMediaplay.Width
frmOutScreen.WindowsMediaPlayer1.Height = frmMediaplay.Height
frmOutScreen.Refresh
End Sub
 
4 编译运行就可以实现frmOutScreen在第二个显示器的输出,你可以加入你的web组件实现浏览器在第二个屏幕输出,看如下我的执行结果
 
 


好了,就这些了,要实现更复杂的功能就需要你自己一点点的去调试了.