Windows系統下的多顯示器模式開發日記

   這幾天研究了一下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組件實現瀏覽器在第二個屏幕輸出,看如下我的執行結果
 
 

好了,就這些了,要實現更復雜的功能就需要你自己一點點的去調試了.

開發日記後記
 
    斷斷續續大約用了一週的零散時間完成了多顯示器支持的功能,達到可以使用的目的,並可以分享給大家,再此作幾點總結作爲結束。
 
1 不熟悉的技術先不要急着去進行代碼的實現,要先搞懂原理;
2 多使用現在已經有得資料,主要是系統幫助,就如MSDN,已經提供了很多有用的東西;
3 要多使用互聯網查找資料,尤其是國外相關網站的資料;
4 要熟悉自己使用的設備;
5 膽大心細,多進行實驗;
6 不懂就問啊,實際上有許多人都已經作了N多的工作了啊
7 最後一點,工作好,休息好,心情好,很重要,哈哈哈
8 別忘記分享給大家。。。。。
 
結束,開始休息
 
(一隻老虎寫於日記之後)

聞香止步 木雕 收集於:http://blog.sina.com.cn/s/blog_4078ccd60100049i.html~type=v5_one&label=rela_nextarticle
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章