現(xiàn):消息機(jī)制與實(shí)戰(zhàn)避坑)
簡介一份基于VC 6.0的MFC對話框工程演示如何借助CStatic圖片控件和BMP位圖實(shí)現(xiàn)綠、黃、紅三色狀態(tài)指示燈面向初學(xué)MFC的C開發(fā)者也適合需要在界面程序里加入運(yùn)行狀態(tài)反饋的程序員。壓縮包內(nèi)含31個(gè)文件約2.22MB其中5個(gè)頭文件與4個(gè)cpp源文件覆蓋對話框類、狀態(tài)切換等核心代碼dsp、dsw負(fù)責(zé)工程配置rc與res目錄存放對話框及指示燈位圖資源另有編譯好的exe程序和說明文件便于對照源碼運(yùn)行調(diào)試。工程通過位圖切換函數(shù)動態(tài)更換三色圖片配合事件處理邏輯實(shí)現(xiàn)正常、警告、錯(cuò)誤三種指示效果代碼結(jié)構(gòu)簡潔、接口清晰可直接遷移到實(shí)際項(xiàng)目中復(fù)用。已有2397人學(xué)習(xí)下載適合希望掌握圖片控件用法、位圖資源加載與界面狀態(tài)聯(lián)動的MFC入門開發(fā)者。1. MFC指示燈一個(gè)讓對話框生動起來的自繪控件起點(diǎn)把“指示燈”做在 MFC 對話框里第一反應(yīng)往往是貼張位圖。但真做完一套狀態(tài)切換你很快會發(fā)現(xiàn)位圖會拉伸、透明色難摳、換圖時(shí)機(jī)還會跟按鈕焦點(diǎn)打架。指示燈這個(gè)小控件放在 MFC 消息循環(huán)里就是一次自繪控件的縮影顏色、背景、狀態(tài)刷新、窗口尺寸變化都集中在幾十行繪制代碼里。它適合所有剛接觸 MFC 自繪控件的開發(fā)者也適合需要快速在儀表盤界面加狀態(tài)點(diǎn)的項(xiàng)目。下面這份實(shí)現(xiàn)筆記把三種常見做法比一遍再給出一份可以直接復(fù)用的 CStatusLight 實(shí)現(xiàn)以及五個(gè)從實(shí)際調(diào)試?yán)飵С鰜淼目?。代碼量不大但每一步都能看得見效果這也是很多人把指示燈當(dāng)成 MFC 自繪控件事后必做的事。2. 方案選型自繪、位圖切換與背景填充哪種指示燈最值得寫2.1 三種常見實(shí)現(xiàn)思路的工作原理MFC 對話框里放指示燈實(shí)際可行的方案無非三條路。第一種是靜態(tài)文本控件配合系統(tǒng)顏色通知。CStatic 本身不提供設(shè)置背景色的成員函數(shù)顏色控制是在父窗口處理 WM_CTLCOLORSTATIC 消息時(shí)通過返回畫刷來實(shí)現(xiàn)的。這個(gè)做法能改整塊矩形背景程序?qū)懙靡部斓霾怀鰣A形“燈珠”的造型狀態(tài)頻繁變化時(shí)刷新還容易出現(xiàn)延遲燈已經(jīng)切到告警紅了畫面還停在綠色。第二種是位圖切換。準(zhǔn)備開、關(guān)兩張位圖控件狀態(tài)一變就換位圖資源并觸發(fā)重繪。視覺效果可以做得精細(xì)但位圖要作為資源打進(jìn)可執(zhí)行文件分辨率和 DPI 一變就容易發(fā)糊還要處理透明像素的摳圖。更現(xiàn)實(shí)的問題是如果指示燈帶閃爍動作位圖切換的節(jié)奏很難跟定時(shí)器完全對齊快速來回切會一閃一暗看起來像屏幕在跳。第三種是自繪 CStatic 子類在 OnPaint 里用 CDC 畫橢圓。顏色、尺寸、高光、陰影都由繪制代碼控制沒有資源文件參與對話框無論縮放到什么尺寸都不變形狀態(tài)切換只需要改布爾變量然后觸發(fā)一次重繪。這是我最常用的一條路徑后面的 CStatusLight 類也基于這種方案。下面用一張表把三種方案的取舍列清楚方便你按現(xiàn)場條件選方案實(shí)現(xiàn)核心視覺效果資源占用維護(hù)成本靜態(tài)文本變色處理 WM_CTLCOLORSTATIC矩形色塊無立體感最低低但刷新易出問題位圖切換多張 BITMAP 資源細(xì)節(jié)豐富但縮放易糊中等換圖邏輯分散自繪 CStaticOnPaint 畫橢圓可控高光與陰影低繪制代碼集中在類內(nèi)2.2 選自繪方案前需要懂的三個(gè) MFC 消息機(jī)制自繪控件不是把畫圓代碼塞進(jìn)去就能跑。MFC 的控件繪制由 WM_PAINT 消息驅(qū)動CStatic 默認(rèn)的窗口過程只是簡單重繪文本內(nèi)容不會留給你“畫一顆圓”的口子所以第一步要重載 OnPaint這是所有自繪動作的入口。第二個(gè)機(jī)制是 WM_ERASEBKGND。系統(tǒng)在繪制控件前默認(rèn)會用窗口背景色擦一遍客戶區(qū)如果 OnPaint 里畫了圓而擦除背景的動作又發(fā)生在 OnPaint 之前兩件事疊加就會出現(xiàn)閃爍和顏色殘留。自繪指示燈通常需要重載 OnEraseBkgnd 并返回 TRUE告訴系統(tǒng)“背景不用你管我在 OnPaint 里自己處理”這是消滅黑底的關(guān)鍵。第三個(gè)機(jī)制是控件的失效通知。CStatic 默認(rèn)不帶交互通知但指示燈的狀態(tài)往往由外部函數(shù)驅(qū)動比如 SetLit(TRUE)。函數(shù)里改了成員變量還不夠必須調(diào)用 Invalidate() 觸發(fā)一次 WM_PAINT否則狀態(tài)變了畫面卻停在原樣。理解了這三個(gè)機(jī)制再看后面的實(shí)現(xiàn)代碼每一步就都能對上號。還有一個(gè)經(jīng)常被混淆的點(diǎn)很多人會用 CButton 的 OwnerDraw 來做指示燈。CButton 自繪要處理 DrawItem繪制區(qū)域是按鈕矩形按鈕獲得焦點(diǎn)時(shí)還會觸發(fā)焦點(diǎn)狀態(tài)繪制燈在點(diǎn)擊后會冒出一圈虛線邊框。雖然能用樣式去掉焦點(diǎn)框但繪制時(shí)機(jī)始終和按鈕交互耦合在一起。如果需求只是一個(gè)不會響應(yīng)輸入的指示燈CStatic 子類是更對口的載體省掉一堆跟按鈕狀態(tài)相關(guān)的分支。2.3 從實(shí)際維護(hù)看自繪方案為什么很少翻車我見過不少用位圖方案做指示燈的界面最后都在維護(hù)階段出了同一個(gè)問題狀態(tài)數(shù)量增加比如從開/關(guān)擴(kuò)展到告警、預(yù)熱兩種中間態(tài)每增加一個(gè)狀態(tài)就要新增一張位圖資源文件里一堆 IDB_ 前綴的圖標(biāo)代碼里 switch 也跟著膨脹。自繪方案不存在這個(gè)問題多一個(gè)狀態(tài)就多一個(gè)顏色值而已。有一個(gè)細(xì)節(jié)必須注意自繪控件的背景色最好和對話框背景保持一致。如果對話框用的是系統(tǒng)窗口色而你給指示燈設(shè)了純白背景放上去就會看到一個(gè)白色方塊邊角。所以我在 CStatusLight 里默認(rèn)把背景色和父對話框底色統(tǒng)一控件貼上去才像“嵌在面板里的一顆燈”。再加上自繪方案繞開了 WM_CTLCOLORSTATIC 的顏色回傳機(jī)制——那個(gè)消息能改的只有文本和背景色畫不出圓——所以只要需求里有圓形燈珠靜態(tài)文本控件這一條路就可以直接否決。3. 核心實(shí)現(xiàn)封裝一個(gè) CStatusLight讓狀態(tài)與繪制徹底分離3.1 頭文件與類設(shè)計(jì)先從類聲明看起。CStatusLight 繼承 CStatic對外只暴露四個(gè)方法SetLit 設(shè)置點(diǎn)亮/熄滅SetColors 一次性設(shè)置開、關(guān)、背景三種顏色SetInterval 控制閃爍節(jié)奏SetBlink 讓控件進(jìn)入閃爍模式。內(nèi)部成員全部是簡單類型沒有指針、沒有資源句柄這樣控件析構(gòu)時(shí)不會泄漏 GDI 對象。// StatusLight.h #pragma once class CStatusLight : public CStatic { public: CStatusLight(); virtual ~CStatusLight(); void SetLit(BOOL bLit); void SetColors(COLORREF clrOn, COLORREF clrOff, COLORREF clrBack); void SetInterval(UINT nMs); protected: afx_msg void OnPaint(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg void OnTimer(UINT_PTR nIDEvent); DECLARE_MESSAGE_MAP() private: BOOL m_bLit; BOOL m_bBlinking; UINT m_nInterval; UINT_PTR m_nTimerId; COLORREF m_clrOn; COLORREF m_clrOff; COLORREF m_clrBack; };這里有個(gè)容易被忽略的點(diǎn)m_nTimerId 沒有寫死成常量而是運(yùn)行時(shí)從控件 ID 取。這樣做的好處是同一個(gè)對話框里放多個(gè)指示燈各自用 GetDlgCtrlID() 作為定時(shí)器 ID互不沖突。類設(shè)計(jì)上堅(jiān)持“狀態(tài)變量只管記錄繪制只管讀取”后面改閃爍邏輯時(shí)不用動繪制函數(shù)。3.2 繪制函數(shù)橢圓、高光與陰影是怎么畫出來的繪制是這個(gè)工具的核心。OnPaint 里先拿客戶區(qū)矩形然后按當(dāng)前狀態(tài)決定填充色依次畫背景矩形、橢圓主體、高光弧線和底部陰影。注意順序不能亂背景永遠(yuǎn)第一個(gè)畫陰影和高光要在橢圓畫完之后再疊否則會被主體色蓋住。// StatusLight.cpp 部分實(shí)現(xiàn) void CStatusLight::OnPaint() { CPaintDC dc(this); CRect rc; GetClientRect(rc); // 先鋪背景避免上層繪制殘留上一幀顏色 dc.FillSolidRect(rc, m_clrBack); int cx rc.Width(); int cy rc.Height(); int radius (cx cy ? cx : cy) / 2 - 3; if (radius 0) return; CPoint center(cx / 2, cy / 2); COLORREF clrFill m_bLit ? m_clrOn : m_clrOff; CBrush brush(clrFill); CBrush* pOldBrush dc.SelectObject(brush); dc.Ellipse(center.x - radius, center.y - radius, center.x radius, center.y radius); dc.SelectObject(pOldBrush); // 高光用一段白色弧線模擬玻璃反射 CPen hiPen(PS_SOLID, 2, RGB(255, 255, 255)); CPen* pOldPen dc.SelectObject(hiPen); dc.Arc(center.x - radius / 2, center.y - radius / 2, center.x radius / 2, center.y radius / 2, center.x, center.y - radius / 2, center.x - radius / 2, center.y); dc.SelectObject(pOldPen); // 陰影底部一條暗線提升立體感 CPen loPen(PS_SOLID, 1, RGB(60, 60, 60)); dc.SelectObject(loPen); dc.MoveTo(center.x - radius / 2, center.y radius / 2); dc.LineTo(center.x radius / 2, center.y radius / 2); dc.SelectObject(pOldPen); }幾個(gè)參數(shù)值得單獨(dú)說。radius 取了寬高的最小值除以 2 再減 3這個(gè)減 3 是給外圈高光流出邊距否則光弧會貼著客戶區(qū)邊界被裁剪。高光弧線不是標(biāo)準(zhǔn)圓弧用的是 Arc 的起點(diǎn)和終點(diǎn)參數(shù)往內(nèi)收半圓視覺上像左上方向的一道反光顏色固定為純白因?yàn)橹甘緹舻牧炼葘Ρ戎饕芍黧w的開關(guān)色承載白色高光在所有配色下都不會突兀。如果想讓燈更圓潤可以把主體橢圓再往里縮 1 像素然后把高光筆寬從 2 改成 1。不過經(jīng)驗(yàn)是筆寬降到 1 后高光在較小控件上看不清楚建議控件直徑小于 20 像素時(shí)直接取消高光弧線。3.3 對外接口與刷新時(shí)機(jī)接口實(shí)現(xiàn)的套路一致改狀態(tài)變量然后 Invalidate(FALSE)。這個(gè) FALSE 是第二個(gè)參數(shù)表示“不擦除背景直接重繪”。因?yàn)槲覀円呀?jīng)在 OnPaint 里鋪了背景再讓系統(tǒng)擦一遍就是白費(fèi)這個(gè)參數(shù)直接影響刷新性能。void CStatusLight::SetLit(BOOL bLit) { m_bLit bLit; if (!m_bLit) m_bBlinking FALSE; Invalidate(FALSE); } void CStatusLight::SetColors(COLORREF clrOn, COLORREF clrOff, COLORREF clrBack) { m_clrOn clrOn; m_clrOff clrOff; m_clrBack clrBack; Invalidate(FALSE); }SetLit 里還做了個(gè)約束當(dāng)燈被強(qiáng)行熄滅時(shí)把閃爍標(biāo)志一并清掉。這樣外部邏輯先調(diào) SetLit(FALSE) 再調(diào) SetLit(TRUE) 不會出現(xiàn)“熄滅狀態(tài)下定時(shí)器還在翻轉(zhuǎn)燈狀態(tài)”的怪象。這個(gè)細(xì)節(jié)是典型的調(diào)試期教訓(xùn)換來的實(shí)際項(xiàng)目里很多偶發(fā)問題都集中在這幾個(gè)布爾標(biāo)志上。對話框里使用這個(gè)控件的步驟也不復(fù)雜在資源編輯器里拖一個(gè) Static Text設(shè)置唯一 ID比如 IDC_STATUS_LIGHT然后在對話框頭文件里把控件變量類型改成 CStatusLightDoDataExchange 里自動生成的 DDX_Control 就把它實(shí)例化了。接下來在 OnInitDialog 里調(diào) SetColors 和 SetLit一個(gè)可用指示燈就位。4. 避坑排查指示燈變黑塊、閃爍失效、背景殘留的五個(gè)現(xiàn)場4.1 現(xiàn)象控件變成灰底黑塊邊緣還有殘影第一次把 CStatusLight 放進(jìn)對話框最常見的結(jié)果不是圓燈而是一塊帶黑邊的灰色矩形甚至周圍一圈白底。原因是默認(rèn)的 CStatic 窗口把 WM_ERASEBKGND 交給系統(tǒng)處理系統(tǒng)按對話框背景色擦一次而 OnPaint 又畫了一次兩次結(jié)果疊加在同一區(qū)域就互相覆蓋出臟底色。解決方法是重載 OnEraseBkgnd 直接返回 TRUE并在 OnPaint 里顯式 FillSolidRect 畫背景。這兩步必須成套出現(xiàn)。只返回 TRUE 不畫背景控件會留下上一幀顏色只畫背景不攔截擦除閃爍依然存在。搭配完成后背景色由 m_clrBack 統(tǒng)一管理對話框什么底色就給這個(gè)成員賦什么值。4.2 現(xiàn)象閃爍開關(guān)時(shí)而靈時(shí)而不靈多個(gè)指示燈串?dāng)_SetBlink 開啟后一個(gè)燈閃起來另一個(gè)燈也跟著閃或者點(diǎn)了關(guān)閉閃爍還在繼續(xù)。這類問題 90% 是定時(shí)器 ID 沖突。MFC 的 SetTimer 按窗口句柄和定時(shí)器 ID 定位回調(diào)如果多個(gè)指示燈都用了同一個(gè)魔法數(shù)字 ID系統(tǒng)會把消息發(fā)給其中最先生效的那個(gè)控件。解決思路我在頭文件里已經(jīng)埋好了用 GetDlgCtrlID() 作為定時(shí)器 ID。每個(gè)控件的資源 ID 在對話框內(nèi)唯一自然避免了沖突。另一條要注意的是KillTimer 必須和 SetTimer 配對出現(xiàn)在同一個(gè)控件的成員函數(shù)里不要在父窗口的 OnDestroy 里統(tǒng)一清理否則一個(gè)燈注銷了定時(shí)器另一個(gè)燈還在等回調(diào)。4.3 現(xiàn)象控件尺寸為零時(shí)燈悶聲不響甚至觸發(fā)斷言O(shè)nPaint 里那句 radius 0 的早退不是隨便寫的。當(dāng)對話框初始化階段還沒執(zhí)行布局時(shí)控件的寬高可能是 0或者用戶在資源編輯器里把 Static 拉成一條線時(shí)GetClientRect 拿到的矩形寬度就是 0。這時(shí)候畫橢圓沒有意義但不少程序在這里直接崩掉因?yàn)?CDC 的 Ellipse 接收負(fù)半徑參數(shù)會觸發(fā)斷言。解決是雙重保險(xiǎn)OnPaint 里保留尺寸判斷同時(shí)父窗口在 OnInitDialog 之后調(diào)用 SetWindowPos 固定控件最小尺寸。我在類里沒有加 SetMinimumSize 這類接口因?yàn)閷?shí)際需求里指示燈尺寸由布局決定類只需要做到“尺寸不合法時(shí)不畫不崩”。4.4 現(xiàn)象燈一變顏色畫面閃得厲害閃爍的直接原因是 WM_ERASEBKGND 和 WM_PAINT 反復(fù)交替。雖然 OnEraseBkgnd 已經(jīng)返回 TRUE但如果父對話框有自己的背景擦除邏輯子控件依舊會被牽連。表現(xiàn)通常是燈本體不閃但燈周圍的對話框區(qū)域閃。這種情況的處理是不為 OnPaint 引入額外雙緩沖邏輯而是先確認(rèn)消息攔截。常見做法是在對話框的 OnEraseBkgnd 里也返回 TRUE并在對話框 OnPaint 里整體刷新背景。如果界面元素很多另一個(gè)辦法是給整個(gè)對話框啟用 WS_CLIPCHILDREN 樣式讓系統(tǒng)不要把子控件區(qū)域納入父窗口重繪范圍。這個(gè)樣式可以在資源編輯器里改對話框?qū)傩砸部梢?ModifyStyle 動態(tài)加。4.5 現(xiàn)象換了一臺高 DPI 電腦橢圓變形、高光錯(cuò)位癥結(jié)在于繪制代碼里所有尺寸都跟著客戶區(qū)像素走而舊系統(tǒng)沒有做 DPI 縮放適配。半徑用寬高最小值計(jì)算主體圓還是正圓但高光弧線的起點(diǎn)和終點(diǎn)用的是相對半徑的固定百分比縮放后視覺就會偏移。這個(gè)問題在真機(jī)上最容易暴露成“燈珠圓了反光卻跑偏”。我的處理習(xí)慣是讓高光參數(shù)不從硬編碼坐標(biāo)來而是把弧線參數(shù)作為半徑的固定比例這樣 DPI 變化時(shí)比例不變形變就控制在穩(wěn)定范圍。另外高 DPI 下把高光筆寬從 2 改為 max(1, cy / 30)能讓光照線寬隨控件尺寸自適應(yīng)。這兩個(gè)小改動不涉及全局 DPI 感知最適合放在類內(nèi)部。5. 進(jìn)階用法閃爍節(jié)奏、過渡效果與多指示燈復(fù)用5.1 定時(shí)器驅(qū)動閃爍的正確寫法閃爍的實(shí)質(zhì)是周期翻轉(zhuǎn) m_bLit 并觸發(fā)重繪。SetTimer 的間隔從 SetInterval 讀取默認(rèn)我給 500ms這個(gè)值對“慢呼吸”和“快速告警”都能通過接口改。派生類只需要重寫 OnTimer判斷定時(shí)器 ID 匹配后翻轉(zhuǎn)狀態(tài)別的控件收到的定時(shí)器消息全部交給基類兜住。void CStatusLight::SetBlink(BOOL bBlink) { if (bBlink !m_bBlinking) { m_bBlinking TRUE; m_nTimerId GetDlgCtrlID(); SetTimer(m_nTimerId, m_nInterval, nullptr); } else if (!bBlink m_bBlinking) { m_bBlinking FALSE; KillTimer(m_nTimerId); m_nTimerId 0; m_bLit TRUE; Invalidate(FALSE); } } void CStatusLight::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent m_nTimerId m_bBlinking) { m_bLit !m_bLit; Invalidate(FALSE); } CStatic::OnTimer(nIDEvent); }SetBlink(FALSE) 的時(shí)候把 m_bLit 強(qiáng)制拉回 TRUE是為了避免退出閃爍后指示燈停在熄滅態(tài)顯得像設(shè)備故障。如果業(yè)務(wù)需要退出后保留當(dāng)前亮滅狀態(tài)把這個(gè)賦值去掉即可。定時(shí)器間隔小于 80ms 時(shí)人眼開始覺得閃爍像抖動而且顯示器刷新率還可能讓燈看起來在移動我一般不建議把閃爍間隔調(diào)低于 100ms。5.2 用內(nèi)存 DC 過渡讓指示燈更耐看直接改變顏色值會帶來跳變。想要柔和一點(diǎn)的過渡效果常見做法是連續(xù)改變 RGB 分量的中間值并把每個(gè)中間幀輸出到內(nèi)存 DC再一次 BitBlt 提交到屏幕。MFC 里用 CreateCompatibleDC 配合 CreateCompatibleBitmap 做雙緩沖很順手。思路是先取當(dāng)前狀態(tài)顏色作為起點(diǎn)目標(biāo)狀態(tài)顏色作為終點(diǎn)按 8 步插值生成中間色每次生成后畫一個(gè)完整橢圓到內(nèi)存 DC再把內(nèi)存 DC 內(nèi)容 BitBlt 到控件 DC。整個(gè)過程在定時(shí)器里跑完所以步數(shù)不能太多8 步 30ms 間隔已經(jīng)能獲得無明顯跳變的過渡。這個(gè)技巧只適合點(diǎn)亮/熄滅頻率不高的場景告警狀態(tài)需要即時(shí)視覺反饋用直接跳變反而更醒目。5.3 把指示燈做成可復(fù)用組件如果同一套界面要放十來個(gè)不同顏色的指示燈建議把 CStatusLight 再包一層面板類面板類持有若干 CStatusLight 實(shí)例按行列做布局對外提供 AddLight 和 SetLightState 接口。這樣對話框里只需要一個(gè)面板控件具體指示燈數(shù)量、顏色、邏輯全部封裝在組件內(nèi)部。布局時(shí)的注意點(diǎn)是每個(gè)子燈都必須在對話框模板或 Create 時(shí)指定唯一 ID。為了方便批量創(chuàng)建可以在面板類里用一個(gè)靜態(tài)計(jì)數(shù)器生成 ID 序列。建議把 ID 從 0x4000 起步避開資源編輯器默認(rèn)的 IDC_ 常量段可以避免不小心撞上對話框其他控件的 ID。到這里指示燈已經(jīng)從一個(gè)畫圓的小函數(shù)變成了可以隨對話框復(fù)用的標(biāo)準(zhǔn)小部件。6. 驗(yàn)收技巧讓狀態(tài)可觀測讓繪制可復(fù)測6.1 狀態(tài)變量與繪制函數(shù)單向前置自繪控件出問題最隱蔽的類型是“燈看起來沒反應(yīng)但邏輯狀態(tài)其實(shí)已經(jīng)變了”。這類 bug 全部源于狀態(tài)與繪制耦合在同一個(gè)函數(shù)里。我在 CStatusLight 里把所有狀態(tài)變量都定義為私有成員OnPaint 只讀它們所有修改集中在 SetLit、SetColors、SetBlink 三個(gè)入口。這樣代碼審查時(shí)只需要看三個(gè)函數(shù)不用全盤掃描。6.2 三件套驗(yàn)證習(xí)慣第一件是日志。在 SetLit 和 OnTimer 里加一行 TRACE(_T(Light %d: %d\n), GetDlgCtrlID(), m_bLit)觀察日志里狀態(tài)翻轉(zhuǎn)次數(shù)和界面閃爍是否一致不一致立刻能定位到是哪一端丟消息。第二件是斷言。在 OnPaint 開始處加 ASSERT(m_clrBack ! 0)可以防止成員變量未初始化就繪制導(dǎo)致的異常色塊。對話框里所有控件變量應(yīng)保證在 DoDataExchange 之后、首次顯示之前完成顏色配置。第三件是截圖對比。給控件狀態(tài)變化拍前后幀對比時(shí)打開系統(tǒng)的顯示縮放選項(xiàng)高 DPI 下的位移問題在第一眼就能看出來。這套檢查習(xí)慣我每次做 MFC 自繪控件都強(qiáng)制走一遍尤其是“所有狀態(tài)從入口函數(shù)流出”這條幾乎能過濾掉一半的偶發(fā)顯示 bug。從那以后我把這個(gè)檢查當(dāng)成指示燈工程的默認(rèn)門檻希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取