Windows系统下多显示器模式开发
轉(zhuǎn)載:開發(fā)日記地址 http://blog.sina.com.cn/s/blog_4078ccd60100049a.html
這幾天研究了一下Windows系統(tǒng)的多顯示器模式的編程,實(shí)現(xiàn)了Windows下支持10顯示器模式的通用com組件,這里做一個整理和回顧,希望能對再這方面開發(fā)的兄弟們有些啟發(fā)和幫助:
(一) Windows系統(tǒng)下的多顯示器模式的原理
Microsoft新的操作系統(tǒng)(Windows 98\Windows 2000\Windows XP)內(nèi)置了對多監(jiān)視器的支持,即用戶可以在一臺計算機(jī)上安裝多個顯示卡并接上多個顯示器,然后把這些顯示器的顯示區(qū)域組織成一個大的虛擬的Windows桌面。每一個顯示區(qū)域的底部都有系統(tǒng)任務(wù)欄,我們可以在任何一個顯示區(qū)域內(nèi)增加桌面快捷方式,這樣就可以在第一個顯示區(qū)域上用Visual C++編程,同時在第二個顯示區(qū)域上打開Internet Explorer上網(wǎng)——再也不用進(jìn)行麻煩的切換了。
多顯示器模式的原理實(shí)際上很簡單,主要還是要靠操作系統(tǒng)的支持,比如WinXP就支持10個顯示器,本文所使用的調(diào)試和開發(fā)環(huán)境都是以WinXP為主,其余的原理都相同慢慢調(diào)試就行了.
Windows提供的多顯示器模式主要有以下三個功能:
1.更大的Windows桌面:在多顯示器模式下,可以把多個顯示器的顯示區(qū)域結(jié)合在一起來顯示W(wǎng)indows桌面,不管這些顯示器的尺寸、物理位置、分辨率和刷新頻率是否相同。當(dāng)我們運(yùn)行一個應(yīng)用程序時,程序的主窗口可以位于任何一個顯示器的顯示區(qū)域內(nèi),也可以跨多個顯示區(qū)域。我們也可以把一個程序的窗口從一個顯示區(qū)域移到另一個顯示區(qū)域中。
2. 屏幕復(fù)制或遠(yuǎn)程顯示:我們可以讓兩個顯示器顯示相同的內(nèi)容。在進(jìn)行培訓(xùn)或者向眾人進(jìn)行演示時,這個特點(diǎn)是很有用的。利用這個特性,技術(shù)支持人員還可以對應(yīng)用程序進(jìn)行遠(yuǎn)程監(jiān)視和調(diào)試。
3.多重獨(dú)立顯示:在以上的兩種模式下,所有的顯示區(qū)域都是Windows虛擬桌面的一部分,但是在多重獨(dú)立顯示模式下,應(yīng)用程序訪問的顯示器并不屬于Windows虛擬桌面。假設(shè)系統(tǒng)的第二個顯示器是一個高分辨率的大尺寸顯示器,我們可以把它用做CAD應(yīng)用程序的專用顯示。通過在CAD應(yīng)用程序中調(diào)用新的Windows API,我們可以借助GDI在上面畫圖。獨(dú)立顯示器的顯示區(qū)域沒有桌面上的任何對象(任務(wù)欄和快捷方式),它與Windows桌面是獨(dú)立的。這可以避免Windows桌面對應(yīng)用程序輸出的任何干擾,我們也不用擔(dān)心會在無意中把其它的窗口拽到獨(dú)立顯示的顯示區(qū)域中,這種方式就好像為應(yīng)用程序提供了一個專用的顯示器。
?
(二)理解虛擬桌面(Virtual Desktop)及其坐標(biāo)
既然是要對多顯示器模式進(jìn)行編程和開發(fā),那么我們就要首先理解Windows的虛擬桌面(Virtual Desktop)及其坐標(biāo)了.這是我們編程開發(fā)的基礎(chǔ),理解了一切就很順利了,幾乎沒有什么難度.
在單顯示器系統(tǒng)中,實(shí)際Windows桌面的形狀和大小與顯示器是相同的。在多顯示器模式下,每一個顯示器實(shí)際上是一個大虛擬桌面的一個“子視窗”。
我們可以通過控制面板中的顯示器屬性對每一個顯示器的顯示區(qū)域的大小(分辨率)和相對位置進(jìn)行調(diào)整,所有這些顯示區(qū)域互相連接但并不重疊。圖一中的顯示器1是主顯示器,主顯示器的作用是確定虛擬桌面的坐標(biāo)。不管主顯示器的位置如何,它的顯示區(qū)域的左上角的坐標(biāo)定為虛擬坐標(biāo)的零點(diǎn)(0,0),右下角的坐標(biāo)是(X-1,Y-1)(假設(shè)主顯示器的分辨率為X×Y),其余顯示區(qū)域的坐標(biāo)由它和主顯示器的相對位置決定。通常虛擬桌面中顯示區(qū)域的相對位置和實(shí)際顯示器的物理相對位置是相同的。因?yàn)樗酗@示區(qū)域必須相連,因此可以用一個包含所有顯示區(qū)域的最小矩形來表示虛擬桌面的大小。圖一中的矩形邊界代表了虛擬桌面的范圍。
因?yàn)樘摂M桌面中的坐標(biāo)系統(tǒng)必須是連續(xù)的,因此第二個顯示區(qū)域的坐標(biāo)是主顯示器的顯示區(qū)域的繼續(xù)。假設(shè)兩個顯示器都使用1024×768的分辨率,并且第二個顯示器位于第一個顯示器(主顯示器)的正右方,則第二個顯示區(qū)域的坐標(biāo)是從(1024,0)到(2047,767)。
但是并不是所有的顯示區(qū)域都具有相同的分辨率,而且這些顯示區(qū)域也不一定是底邊對齊的。就像圖一中顯示的那樣,你真正能看到的有效顯示區(qū)域是紅色+蘭色+紫色的不規(guī)則區(qū)域,而黃色區(qū)域雖然也屬于虛擬桌面的一部分,但它不屬于任何一個顯示區(qū)域,這部分也叫做無效區(qū)域。如圖一中所示,假設(shè)顯示器1的分辨率是1024×768,顯示器2的分辨率為800×600,顯示器3的分辨率為640×480。零點(diǎn)的位置如圖中所示,顯示器1的坐標(biāo)為(0,0)到(1023,767),顯示器2的坐標(biāo)為(-800,168)到(-1,767),顯示器3的坐標(biāo)是(1024,0)到(1663,479)。而(-800,0)到(-1,167)以及(1024,480)到(1663,767)這兩塊無效區(qū)域是不能顯示任何信息的,系統(tǒng)不會允許用戶把鼠標(biāo)移動到這兩個區(qū)域。需要注意的是無效區(qū)域是包括在虛擬桌面中的,因此圖一中的虛擬桌面的大小是從(-800,0)到(1663,767)。
我在編程開發(fā)的過程中就使用了2個顯示器,一個是自己的筆記本,分辨率為1024×768作為主顯示器,另外一個由于比較懶,直接找了一個小巧的NEC12寸屏幕的小黑白顯示器,不是為了別的搬著方便啊,這個NEC黑白支持分辨率800×600,強(qiáng)吧.
如下圖我是直接設(shè)置了擴(kuò)展桌面,兩個顯示器就都可以使用了
?
在這里要注意主顯示器和副顯示器的區(qū)別,其實(shí)主顯示器和副顯示器你是可以進(jìn)行任意調(diào)整的.
(三)系統(tǒng)支持編程開發(fā)的API
Microsoft為支持多顯示器模式提供了一些新的API調(diào)用,下面具體介紹它們的功能:
1.HMONITOR MonitorFromPoint(POINT pt,DWORD dwFlags)
MonitorFromPoint返回包含特定點(diǎn)(pt)的一個顯示器句柄。如果pt不屬于任何一個顯示器,返回的顯示器句柄由dwFlags標(biāo)志決定:MONITOR_DEFAULTTONULL時返回NULL,MONITOR_DEFAULTTOPRIMARY時返回代表主顯示器的HMONITOR句柄,MONITOR_DEFAULTTONEAREST時返回最靠近pt點(diǎn)的顯示器的HMONITOR句柄。
???? 2.HMONITOR MonitorFromRect(LPCRECT lprc,DWORD dwFlags)
MonitorFromRect返回包含lprc代表的矩形的顯示器句柄;如果包含此矩形的顯示區(qū)域不止一個,則返回包含矩形最大部分的顯示器句柄;如果矩形不屬于任何一個顯示區(qū)域,返回的句柄由dwFlags決定,規(guī)則與MonitorFromPoint相同。
3. HMONITOR MonitorFromWindow(HWND hwnd,DWORD dwFlags)
與MonitorFromRect類似,但輸入是一個代表窗口的句柄hwnd而不是指向矩形的指針。
4. BOOL GetMonitorInfo(HMONITOR hMonitor,LPMONITORINFO lpmi)
GetMonitorInfo返回由hMonitor代表的顯示器的有關(guān)信息,這些信息存儲在指向MONITORINFO結(jié)構(gòu)的指針——lpmi中。這些信息包括用RECT結(jié)構(gòu)表示的顯示器的顯示區(qū)域的大小(如果這個顯示器不是主顯示器,RECT的坐標(biāo)可能為負(fù)數(shù)),以及用RECT結(jié)構(gòu)表示的顯示器的工作區(qū)域的大小,工作區(qū)域是顯示區(qū)域中除去系統(tǒng)任務(wù)欄和應(yīng)用程序快捷方式欄所剩下的區(qū)域,還能夠判斷此顯示器是否為主顯示器,并返回一個標(biāo)志。
5.BOOL EnumDisplayMonitors(HDC hdc,LPCRECT lprcClip,MONITORENUMPROC lpfnEnum,LPARAM dwData)
hdc是一個代表顯示設(shè)備環(huán)境的句柄,lprcClip是指向一個矩形區(qū)域的指針。把這個矩形區(qū)域和設(shè)備環(huán)境中的可見區(qū)域取交集,得到的區(qū)域可能分布在多個顯示器的顯示區(qū)域中,EnumDisplayMonitors對每一個包含交集的顯示區(qū)域調(diào)用一次MonitorEnumProc類型的函數(shù)。DwData為傳遞給MonitorEnumProc函數(shù)的數(shù)據(jù)。
6.BOOL CALLBACK MonitorEnumProc(HMONITOR hmonitor,HDC hdcMonitor,LPRC lprcMonitor, DWORD dwData)
MonitorEnumProc是一個被EnumDisplayMonitors函數(shù)調(diào)用的回調(diào)函數(shù),它的內(nèi)容可以由用戶自定義。利用這兩個函數(shù),用戶在進(jìn)行跨多個顯示器的顯示時就可以利用每一個顯示器的不同的顯示特性。
當(dāng)然,并不是所有畫圖程序都必須調(diào)用這兩個函數(shù),這時你假設(shè)所有的顯示器都使用同樣顏色的分辨率。
7.EnumDisplayDevices(LPVOID lpReserved,int iDeviceNum,DISPLAY_DEVICE×pDisplayDevice,DWORD dwFlags)
EnumDisplayDevices列出系統(tǒng)中某個顯示設(shè)備(以iDeviceNum為序號)的信息。與GetMonitorInfo相比,GetMonitorInfo對應(yīng)的顯示器必須是Windows虛擬桌面的一部分,而EnumDisplayDevices可以列出包括處于獨(dú)立顯示模式下的系統(tǒng)所安裝的所有顯示器的信息。它返回的信息儲存在DISPLAY_DEVICE結(jié)構(gòu)中,包括顯示設(shè)備名稱、對顯示設(shè)備的描述和顯示設(shè)備的狀態(tài)。
此外,一些原有的API調(diào)用如SystemParametersInfo和GetSystemMetrics也加入了對多顯示器模式的支持。比如調(diào)用GetSystemMetrics時,如果用SM_XVIRTUALSCREEN、SM_YVIRTUALSCREEN、SM_CXVIRTUALSCREEN和SM_CYVIRTUALSCREEN,得到的是虛擬桌面左上角的坐標(biāo)和整個的長度和寬度。
我們在編程時特別要注意坐標(biāo)的變化:首先單顯示器下負(fù)坐標(biāo)或大于SM_CXSCREEN和SM_CYSCREEN部分的窗口將被隱藏,而在多顯示器模式下這些都是合法的。其次在確定應(yīng)用程序窗口和對話框的位置時,要選擇正確的顯示器和正確的全局坐標(biāo)(虛擬桌面坐標(biāo))。最后,在恢復(fù)原來存儲的窗口之前,要檢查一下這些窗口坐標(biāo)的有效性。
這些都可以在微軟的MSDN上去查出來,需要仔細(xì)的看一看,每個API都親自試一試.
大家可以參考MSND的一篇文章"How to Exploit Multiple Monitor Support in Memphis and Windows NT 5.0",說的很詳細(xì).
?
(四)實(shí)現(xiàn)多屏幕編程的組件設(shè)計
這個組件參考了網(wǎng)上的許多資料,這里先向那些無私的同行表示感謝,我做的工作只是將他們的成果進(jìn)行了系統(tǒng)化的整理......
組件的設(shè)計流程如下:
(1).初始化程序
Syntax:: MScreenInfo();
Description : 部件構(gòu)造函數(shù),初始化部件,獲取系統(tǒng)屏幕信息,設(shè)置部件屬性。
(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). 獲取指定屏幕的坐標(biāo)原點(diǎn)-left
Syntax: Short GetScreenLeft( Short ScreenNo) ;
Input : ScreenNo -- 指定屏幕的序號,0 -- m_monitorNum-1;
Return: Screen Left in Pixel;
Decription: 獲取ScreenNo指定屏幕的坐標(biāo)原點(diǎn)-left。
程序流程圖:與圖2相同,只是最后一步返回dm.dmPosition.x.
(5). 獲取指定屏幕的坐標(biāo)原點(diǎn)-top
Syntax: Short GetScreenLeft( Short ScreenNo) ;
Input : ScreenNo -- 指定屏幕的序號,0 -- m_monitorNum-1;
Return: Screen Top in Pixel;
Decription: 獲取ScreenNo指定屏幕的坐標(biāo)原點(diǎn)-top。
程序流程圖:與圖2相同,只是最后一步返回dm.dmPosition.y.
(6). 獲取主屏幕--Primary Screen
Syntax: Short GetPrimaryScreen();
Input: Null;
Return: Primary Screen No, 0 -- m_monitorNum - 1
Description: 獲取主屏幕的序號。
程序流程:依次判斷那一個屏幕的原點(diǎn)是(0, 0).
?
(五)組件開發(fā)的實(shí)現(xiàn)和主要代碼
1 開發(fā)環(huán)境
操作系統(tǒng): WindowsXP 編程環(huán)境: VC 6.0
2 組件接口如下
3 主要代碼
// 獲得顯示器的數(shù)量
CMScreenInfoCtrl::CMScreenInfoCtrl()
{
InitializeIIDs(&IID_DMScreenInfo, &IID_DMScreenInfoEvents);
// 找出顯示器的總數(shù)量
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; // 總數(shù)量
}
// 獲得顯示區(qū)寬度
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;
}
// 設(shè)置顯示區(qū)寬度
void CMScreenInfoCtrl::SetScreenWidth(short ScreenNo, short nNewValue)
{
SetModifiedFlag();
}
// 獲得顯示區(qū)寬度
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;
}
// 設(shè)置顯示區(qū)高度
void CMScreenInfoCtrl::SetScreenHeight(short ScreenNo, short nNewValue)
{
SetModifiedFlag();
}
// 獲得顯示區(qū)Y坐標(biāo)
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 ;
}
// 設(shè)置顯示區(qū)Y坐標(biāo)
void CMScreenInfoCtrl::SetScreenTop(short ScreenNo, short nNewValue)
{
SetModifiedFlag();
}
// 獲得顯示區(qū)X坐標(biāo)
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 ;
}
// 設(shè)置顯示區(qū)X坐標(biāo)
void CMScreenInfoCtrl::SetScreenLeft(short ScreenNo, short nNewValue)
{
SetModifiedFlag();
}
// 獲得主顯示區(qū)
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;
}
// 設(shè)置主顯示區(qū)
void CMScreenInfoCtrl::SetPrimaryScreen(short nNewValue)
{
SetModifiedFlag();
}
關(guān)鍵的代碼基本就是這些了.
(3)組件發(fā)布
直接編譯成為ocx組件,取名為MutlScreen.ocx
使用regsvr32.exe注冊一下就可以使用了.
?
(六)分屏輸出組件的應(yīng)用
我們使用最簡單的VB來編寫一個小程序?qū)崿F(xiàn)
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 編譯運(yùn)行就可以實(shí)現(xiàn)frmOutScreen在第二個顯示器的輸出,你可以加入你的web組件實(shí)現(xiàn)瀏覽器在第二個屏幕輸出,看如下我的執(zhí)行結(jié)果
好了,就這些了,要實(shí)現(xiàn)更復(fù)雜的功能就需要你自己一點(diǎn)點(diǎn)的去調(diào)試了.
開發(fā)日記后記
斷斷續(xù)續(xù)大約用了一周的零散時間完成了多顯示器支持的功能,達(dá)到可以使用的目的,并可以分享給大家,再此作幾點(diǎn)總結(jié)作為結(jié)束。
1 不熟悉的技術(shù)先不要急著去進(jìn)行代碼的實(shí)現(xiàn),要先搞懂原理;
2 多使用現(xiàn)在已經(jīng)有得資料,主要是系統(tǒng)幫助,就如MSDN,已經(jīng)提供了很多有用的東西;
3 要多使用互聯(lián)網(wǎng)查找資料,尤其是國外相關(guān)網(wǎng)站的資料;
4 要熟悉自己使用的設(shè)備;
5 膽大心細(xì),多進(jìn)行實(shí)驗(yàn);
6 不懂就問啊,實(shí)際上有許多人都已經(jīng)作了N多的工作了啊
7 最后一點(diǎn),工作好,休息好,心情好,很重要,哈哈哈
8 別忘記分享給大家。。。。。
結(jié)束,開始休息
(一只老虎寫于日記之后)
轉(zhuǎn)載于:https://www.cnblogs.com/xuehuzaifei/p/3222807.html
總結(jié)
以上是生活随笔為你收集整理的Windows系统下多显示器模式开发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 混合编译
- 下一篇: Dynamics CRM 2013 初体