目錄
1、專案背景與需求
2、帶透明區域視窗的實作思路
3、duilib界面庫
4、呼叫UpdateLayeredWindow介面回傳失敗
5、點擊標題欄無法拖動視窗
6、選擇區域坐標該怎么設定給底層的影像采集編碼層呢?
你用帶有WS_EX_LAYERED風格的Layered分層視窗實作過異形視窗的效果嗎?在很多軟體中都能看到異形視窗的身影,異形視窗以其獨有的視覺效果,被廣泛地采用,以常見的360安全衛士的加速球視窗為例:

異形視窗的邊界一般是各種形狀的圓滑線條,被做成了各種妙趣橫生的各種圖案效果,給用戶帶來了良好的視覺效果和體驗,
其實,視窗在創建時是矩形形狀的,只不過呈現出來的圖案是各種獨特形狀的,除這些形狀區域以外,矩形視窗的其他區域被做成透明效果了,并且滑鼠是可以穿透這些透明的區域的,本文要講一種特殊的異形視窗,下面請看我詳細道來,
1、專案背景與需求
最近公司中標了一個比較大的專案,客戶對我們的會議軟體總體上是很滿意的,但客戶要求在已有的功能上增加一個功能,要在桌面共享功能中增加桌面區域共享的功能,客戶明確提出,這個需求是硬性功能指標,必須實作這個功能后才能完成專案的中標,于是相關部門將開發需求落到了我們研發部,要求我們在最短的時間內盡快地實作這個功能,
這個桌面區域共享,和整個桌面共享及應用視窗共享實作機制時完全一樣的,都是抓取某一個區域的影像,只要UI層告訴采集編碼層要抓取的區域坐標就可以了,所以主要的作業量在UI層,影像采集編碼層不用做大的改動即可實作,所以這個功能點的實作重心就在于UI層的互動實作,即UI層如何選定要分享的區域,然后都需要支持哪些操作,
這個桌面部磁區域的共享,很多友商都支持了,在時間緊急沒有頭緒的情況下,趕緊看一下友商的實作方式,經過對比發現,小魚易聯和ZOOM的會議軟體,該功能的UI互動和實作機制竟然是一模一樣的,至于誰先做出來、誰模仿誰,就不得而知了,于是大概地看了一下他們的實作機制,創建一個特殊的視窗,視窗是有邊界的,然后視窗的中間區域是透明的、且滑鼠可穿透的,如下所示:(這種顯示背景下的截圖)


該特殊視窗的邊界框住的區域就是要分享的區域,即框住的影像就是要分享出去的影像,通過這個視窗實作了待分享區域的選擇,該選擇區域的視窗支持標題欄拖動,支持拖動邊框改變大小,
初步猜測該視窗應該是具有WS_EX_LAYERED風格的分層視窗,呼叫UpdateLayeredWindow系統API將視窗中間區域透明掉,使用SPY++工具查看了一下該視窗的屬性:

確實是分層視窗,并且設定了TOPMOST視窗置頂的屬性,我們有呼叫UpdateLayeredWindow實作透明視窗的經驗,所以本例中這樣的視窗效果我們也能實作,于是基本擬定了當前桌面區域共享的幾個需求點:
1)選擇區域的視窗使用具有WS_EX_LAYERED視窗樣式的分層視窗,呼叫UpdateLayeredWindow實作視窗中間區域的透明及滑鼠穿透;
2)該選擇區域的視窗,支持標題欄拖動視窗,支持拖動視窗邊框改變視窗大小,
2、帶透明區域視窗的實作思路
這種帶透明區域且滑鼠可穿透的視窗,直接設計出一個帶透明區域的圖片,然后呼叫UpdateLayeredWindow系統API,將圖片貼到目標視窗上即可,
具體的實作思路是,先創建帶有WS_EX_LAYERED視窗樣式的分層視窗(目標視窗),將帶透明區域的圖片繪制到記憶體DC上,中間透明區域則全部是RGB(0,0,0)純黑色,然后呼叫UpdateLayeredWindow將記憶體DC畫板中的內容繪制到目標視窗上,中間黑色的區域會變成透明、滑鼠可穿透的區域,圖片的非透明區域則是不透明的,
但本例中的視窗大小是可變的,需要支持拖動視窗邊框改變大小的,所以不能直接將整個圖片貼到視窗上,因為UCD組(美工組)提供的圖片是固定大小的,不能做縮放操作的,所以我們要從圖片中摳出視窗left、top、right、bottom四個方向上的邊框區域,然后分塊貼到記憶體DC上即可,只要保證中間要透明的區域是黑色的色塊就可以了,貼圖及呼叫UpdateLayeredWindow的相關代碼如下所示:
#define DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT 27
#define DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH 5
#define DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH 5
#define DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT 5
void CDesktopShareAreaSelDlg::Update()
{
if ( m_BkImg.IsNull() )
{
return;
}
RECT rcWnd;
::GetWindowRect(m_hWnd, &rcWnd);
int nWndWidth = rcWnd.right - rcWnd.left;
int nWndHeight = rcWnd.bottom - rcWnd.top;
// Create the alpha blending bitmap
BITMAPINFO bmi; // bitmap header
ZeroMemory(&bmi, sizeof(BITMAPINFO));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = nWndWidth;
bmi.bmiHeader.biHeight = nWndHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32; // four 8-bit components
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biSizeImage = nWndWidth * nWndHeight * 4;
BYTE *pvBits; // pointer to DIB section
HBITMAP hbitmap = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pvBits, NULL, 0);
if (pvBits == NULL) {
return;
}
ZeroMemory(pvBits, bmi.bmiHeader.biSizeImage);
// 創建記憶體DC
HDC hMemDC = CreateCompatibleDC(NULL);
HBITMAP hOriBmp = (HBITMAP)SelectObject(hMemDC, hbitmap);
int nImgWidth = m_BkImg.GetWidth();
int nImgHeight = m_BkImg.GetHeight();
// 將視窗left、top、right、bottom四個方向上的圖片繪制到視窗邊框的位置
m_BkImg.Draw( hMemDC, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nWndHeight, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nImgHeight );
m_BkImg.Draw( hMemDC, nWndWidth - DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nWndHeight, nImgWidth-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nImgHeight );
m_BkImg.Draw( hMemDC, 0, 0, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT, 0, 0, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT );
m_BkImg.Draw( hMemDC, 0, nWndHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, 0, nImgHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT );
POINT ptDst = {rcWnd.left, rcWnd.top};
POINT ptSrc = {0, 0};
SIZE WndSize = {nWndWidth, nWndHeight};
BLENDFUNCTION blendPixelFunction= { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
BOOL bRet= UpdateLayeredWindow(m_hWnd, NULL, &ptDst, &WndSize, hMemDC,
&ptSrc, 0, &blendPixelFunction, ULW_ALPHA);
DWORD dwRet = GetLastError();
InvalidateRect( m_hWnd, &rcWnd, TRUE );
//_ASSERT(bRet); // something was wrong....
// Delete used resources
SelectObject(hMemDC, hOriBmp);
DeleteObject(hbitmap);
DeleteDC(hMemDC);
}
上述介面要在WM_SIZE訊息中呼叫,即視窗大小改變時,要重新繪制視窗:
LRESULT CDesktopShareAreaSelDlg::HandleMessage( UINT message, WPARAM wParam, LPARAM lParam )
{
TNotifyUI msg;
if ( WM_SIZE == message )
{
Update();
}
else if ( WM_PAINT == message )
{
Update();
return true;
}
}
3、duilib界面庫

本文涉及到的很多代碼,都和duilib有關,所以此處需要簡單地說明一下,我們程式UI使用的是開源的duilib界面庫,是基于微軟directui思想的一套界面庫,
做UI的朋友應該大部分都知道這個duilib界面庫,現在很多公司都在用,比如百度、華為、網易、愛奇藝、ZOOM等,這些公司的Windows開發招聘崗位上有對熟悉duilib庫的要求,當然,這些廠商會對duilib進行深入的優化和改進,解決了很多bug,我們這邊也不例外,也使用程序中也發現了很多問題,也對代碼進行了一些優化和改進,
4、呼叫UpdateLayeredWindow介面回傳失敗
在創建CDesktopAreaShareDlg視窗時設定了WS_EX_LAYERED分層視窗的風格,結果運行顯示出來的視窗并沒有顯示圖片邊框,視窗中間也不是透明、滑鼠可穿透的,除錯代碼發現,UpdateLayeredWindow函式呼叫失敗了,GetLastError回傳的錯誤碼是87:

到VS的錯誤查找工具中搜索了一下:

該錯誤碼的含義是:引數錯誤,但詳細檢查了一下傳入的引數值,并沒有例外非法的值,這個就有點奇怪了,
首先創建的位圖是包含Alpha通道的32位位圖,如果是非32位位圖則可能導致UpdateLayeredWindow函式呼叫失敗,其次傳入到UpdateLayeredWindow介面中的引數都沒問題的,可為啥還會失敗呢?
難道是WS_EX_LAYERED視窗風格設定失敗了?于是想用SPY++抓了一下視窗屬性,看看視窗到底有沒有WS_EX_LAYERED風格,但是該視窗因為呼叫UpdateLayeredWindow失敗了,桌面上根本沒顯示這個視窗,沒關系,我們可以直接到SPY++中搜索該視窗,通過創建視窗時使用的類名搜索(只填充類名,將其他輸入框清空):

搜索到該視窗,查看視窗屬性中的視窗風格:

果然沒有WS_EX_LAYERED風格,這就奇怪了,明明設定了WS_EX_LAYERED視窗風格,為啥沒生效呢?
這個視窗是繼承duilib框架中的CAppWidnow(我們自行封裝的,duilib開源代碼是沒有的)通用視窗類的,好像該通用視窗類中有個設定透明度的選項,可能是處理視窗透明度的代碼將WS_EX_LAYERED風格給取消了,
于是到duilib的代碼中,找到設定透明度的代碼,確實是這個設定透明度的介面將WS_EX_LAYERED風格取消了:
void CPaintManagerUI::SetTransparent(int nOpacity)
{
if (NULL == m_hWndPaint)
{
return;
}
typedef BOOL(__stdcall *PFUNCSETLAYEREDWINDOWATTR)(HWND, COLORREF, BYTE, DWORD);
PFUNCSETLAYEREDWINDOWATTR fSetLayeredWindowAttributes;
HMODULE hUser32 = ::GetModuleHandle(_T("User32.dll"));
if (hUser32)
{
fSetLayeredWindowAttributes =
(PFUNCSETLAYEREDWINDOWATTR)::GetProcAddress(hUser32, "SetLayeredWindowAttributes");
if (NULL == fSetLayeredWindowAttributes)
{
return;
}
}
DWORD dwStyle = ::GetWindowLong(m_hWndPaint, GWL_EXSTYLE);
DWORD dwNewStyle = dwStyle;
if (nOpacity >= 0 && nOpacity < 255)
{
dwNewStyle |= WS_EX_LAYERED;
}
else
{
dwNewStyle &= ~WS_EX_LAYERED;
}
if (dwStyle != dwNewStyle)
{
::SetWindowLong(m_hWndPaint, GWL_EXSTYLE, dwNewStyle);
}
fSetLayeredWindowAttributes(m_hWndPaint, 0, nOpacity, LWA_ALPHA);
}
我們給該視窗設定的透明度為255,即不透明,所以上述代碼中發現傳入的透明度引數nOpacity為255,就將WS_EX_LAYERED風格給取消了,此處的代碼是有問題的,將視窗的透明度設定為不透明,不應該將WS_EX_LAYERED風格取消掉,因為可能會呼叫處理分層視窗的其他系統API函式,比如我們本案例用到的UpdateLayeredWindow,所以要將這個取消WS_EX_LAYERED風格的代碼注釋掉,
但代碼注釋后運行,還是有問題,UpdateLayeredWindow介面還是執行失敗了,lasterror值依舊是87,依稀記得,好像設定視窗透明度的介面SetLayeredWindowAttributes和處理異形視窗的介面UpdateLayeredWindow是不能同時呼叫的,如上的代碼所示,設定透明度的介面CPaintManagerUI::SetTransparent中不管是否設定了有效的透明度(小于255),都呼叫了SetLayeredWindowAttributes,所以這點也是不合理的,應該設定不透明時,就不應該呼叫SetLayeredWindowAttributes,修改后的代碼
重新運行代碼后,發現有效果了,視窗邊界圖片顯示出來了,視窗的中間區域也是可穿透的了,
5、點擊標題欄無法拖動視窗
對于視窗支持標題欄拖動、視窗支持拖動邊框改變視窗大小,duilib中的視窗框架都支持,只要在xml檔案中設定兩個屬性就可以了,對于標題欄拖動,設定caption屬性即可,即caption="0,0,0,27";對于視窗邊界可拖動,設定sizebox屬性即可,即sizebox="5,5,5,5",視窗對應的xml檔案如下:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Window size="794,500" mininfo="400,250" sizebox="5,5,5,5" caption="0,0,0,27" shadow="false">
<Font name="微軟雅黑" size="12" />
<Default name="VScrollBar" value="width="10" showbutton1="false" showbutton2="false" thumbnormalimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='20,0,30,21' " bknormalimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " bkhotimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " bkpushedimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " bkdisabledimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " " />
<VerticalLayout name="desktopareaselwnd" bkcolor="#FFFF0000">
</VerticalLayout>
</Window>
添加屬性后,我們測驗了一下效果,視窗邊界拖動視窗大小是沒問題的,但標題欄是無法拖動視窗的,當滑鼠移動到視窗中時,會產生WM_NCHITTEST訊息,按講移到視窗標題欄區域時,應該回傳HTCAPTION,以支持視窗的拖動,
于是到duilib框架代碼中查看CAppWindow類處理WM_NCHITTEST訊息的代碼:
LRESULT CAppWindow::OnNcHitTest( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{
POINT pt;
pt.x = GET_X_LPARAM( lParam );
pt.y = GET_Y_LPARAM( lParam );
::ScreenToClient( *this, &pt );
RECT rcClient;
::GetClientRect( *this, &rcClient );
// 改變大小
if( !::IsZoomed(*this) )
{
RECT rcSizeBox = m_pm.GetSizeBox();
if( pt.y < rcClient.top + rcSizeBox.top )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTTOPLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTTOPRIGHT;
return HTTOP;
}
else if( pt.y > rcClient.bottom - rcSizeBox.bottom )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTBOTTOMLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTBOTTOMRIGHT;
return HTBOTTOM;
}
if( pt.x < rcClient.left + rcSizeBox.left ) return HTLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTRIGHT;
}
// 標題欄回應
RECT rcCaption = m_pm.GetCaptionRect();
if( pt.x >= rcClient.left + rcCaption.left
&& pt.x < rcClient.right - rcCaption.right
&& pt.y >= rcCaption.top
&& pt.y < rcCaption.bottom )
{
// 考慮到標題欄區域會放置控制元件,比如常見的右上角的最小化、最大化和關閉按鈕,
// 所以要將按鈕等控制元件過濾掉
CControlUI* pControl = static_cast<CControlUI*>( m_pm.FindControl( pt ) );
if( pControl
&& _tcscmp(pControl->GetClass(), _T("ButtonUI")) != 0
&& _tcscmp(pControl->GetClass(), _T("OptionUI")) != 0
&& _tcscmp(pControl->GetClass(), _T("TextUI")) != 0 )
{
return HTCAPTION;
}
}
return HTCLIENT;
}
打斷點除錯發現,在判斷滑鼠位置落在標題欄區域時,會判斷滑鼠是否落在某個dui控制元件中,如果回傳的dui控制元件指標為空(滑鼠點擊點是否落在視窗的dui控制元件上),是不會回傳HTCAPTION值的,于是在CDesktopAreaShareDlg視窗類的HandleMesssge中攔截WM_HITTEST訊息,將代碼修改一下,去掉是否落在控制元件中的判斷,我們這個視窗比較簡單,也比較特殊:
LRESULT CDesktopShareAreaSelDlg::HandleMessage( UINT message, WPARAM wParam, LPARAM lParam )
{
TNotifyUI msg;
if ( WM_SIZE == message )
{
Update();
}
else if ( WM_PAINT == message )
{
Update();
return true;
}
else if ( WM_NCHITTEST == message)
{
POINT pt;
pt.x = GET_X_LPARAM( lParam );
pt.y = GET_Y_LPARAM( lParam );
::ScreenToClient( m_hWnd, &pt );
RECT rcClient;
::GetClientRect( m_hWnd, &rcClient );
// 改變大小
if( !::IsZoomed(m_hWnd) )
{
RECT rcSizeBox = m_pm.GetSizeBox();
if( pt.y < rcClient.top + rcSizeBox.top )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTTOPLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTTOPRIGHT;
return HTTOP;
}
else if( pt.y > rcClient.bottom - rcSizeBox.bottom )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTBOTTOMLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTBOTTOMRIGHT;
return HTBOTTOM;
}
if( pt.x < rcClient.left + rcSizeBox.left ) return HTLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTRIGHT;
}
// 標題欄回應
RECT rcCaption = m_pm.GetCaptionRect();
if( pt.x >= rcClient.left + rcCaption.left
&& pt.x < rcClient.right - rcCaption.right
&& pt.y >= rcCaption.top
&& pt.y < rcCaption.bottom )
{
return HTCAPTION;
}
}
return CAppWindow::HandleMessage( message, wParam, lParam );
}
對于我們當前的視窗,只要落在視窗標題欄區域就直接回傳HTCAPTION值了,不用再去做其他的判斷了,
至于為啥會出現滑鼠落在的控制元件指標回傳NULL的情況呢?我們的視窗類CDesktopAreaShareDlg的xml檔案中已經配置了一個垂直根布局(只有這個根布局):
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Window size="794,500" mininfo="400,250" sizebox="5,5,5,5" caption="0,0,0,27" shadow="false">
<Font name="微軟雅黑" size="12" />
<Default name="VScrollBar" value="width="10" showbutton1="false" showbutton2="false" thumbnormalimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='20,0,30,21' " bknormalimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " bkhotimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " bkpushedimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " bkdisabledimage="res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' " " />
<VerticalLayout name="desktopareaselwnd" bkcolor="#FFFF0000">
</VerticalLayout>
</Window>
按講根布局會鋪滿整個視窗的,滑鼠肯定是落在這個根布局上,于是在CAppWidnow處理WM_NCHITTEST訊息的介面中打斷點單步除錯發現,根布局的區域位置m_rcItem為(0,0,0,0),即根布局的區域大小為0,所以不會滑鼠肯定不會落在根布局上,
奧,原來是這樣的,我們xml中控制元件是在所在視窗收到WM_PAINT訊息時去布局控制元件(給控制元件設定位置)的,但一旦對視窗呼叫UpdateLayeredWindow后視窗就不會產生WM_PAINT訊息的,我們會在xml中設定視窗的大小,我們在呼叫CreateWIndow(Ex)將視窗創建起來收WM_CREATE訊息時區加載決議xml檔案,會優先得到xml中設定的視窗大小,會呼叫SetWindowPos去設定視窗的大小,視窗大小會發生變化,就會產生WM_SIZE訊息,而CDesktopAreaShareDlg視窗在收到這個訊息時就會呼叫UpdateLayeredWindow,這樣視窗后面就不會再產生WM_PAINT訊息了,所以CDesktopAreaShareDlg視窗就沒有排布xml中控制元件的機會了,所以根布局控制元件的大小始終是0,
6、選擇區域坐標該怎么設定給底層的影像采集編碼層呢?
選擇區域視窗大小可拖動,選擇可以拖動標題欄拖動視窗,我們在視窗移動時或者視窗大小發生變化時,將選擇區域的坐標實時的設定給影像采集編碼層?如果由UI層呼叫介面將選擇區域的坐標設定給媒控層,則存在兩個問題:
1) 何時觸發呼叫設定選擇區域坐標給影像采集編碼層的介面?在視窗移動時,在視窗大小發生變化時都要觸發,這樣介面呼叫會非常地頻繁,
2) UI層需要呼叫組件API層的介面,然后再經過組件層內部的多層后,再設定到采集編碼庫
中,這些層與層之間的介面都是異步的,很難保證選擇區域的大小,實時地通知給采集編碼層,
其實,有個最好的辦法,UI層給采集編碼層設定用來選擇區域視窗句柄,并將選擇視窗四個邊界的寬度或高度設定給采集編碼層,如下所示:

這樣媒控層可以在每次截圖時實時去獲取選擇視窗的坐標,然后將選擇視窗邊界寬度與高度減掉,就能實時地得到選擇區域的坐標了,這應該是最合理的方式!函式呼叫需要額外的開銷,所以采用這種方式,既可以滿足實時性獲取要求,也能減少函式呼叫的開銷!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/316709.html
標籤:其他
