SWT Button 雖功能豐富,但外觀客製化能力極弱:無法實現漸層背景、特殊文字效果,甚至連邊框都無法移除。嘗試 PaintListener 會在按下瞬間失效;使用圖片則面臨邊框破壞美感、資源浪費、縮放失真等問題;繼承 Canvas 模擬按鈕又需手動維護狀態,且無法融入 JFace Action 體系。本文展示如何利用 Windows 底層 BS_OWNERDRAW 機制,透過繼承 Button 並覆寫 wmDrawChild 實現真正的自繪按鈕,完美保留原生行為與 JFace 整合能力。
Win32 API 中,帶 BS_OWNERDRAW 風格的按鈕會接收 WM_DRAWITEM 訊息,應用程式可在其中完全控制繪製。SWT Button 內部已在 wmDrawChild 中處理此訊息(僅用於 SWT.ARROW 風格),且該方法為 package-private,故我們需將自訂類置於 org.eclipse.swt.widgets 套件下即可覆寫。
package org.eclipse.swt.widgets;
import org.eclipse.swt.internal.win32.*;
public abstract class OwnerDrawButton extends Button {
public OwnerDrawButton(Composite parent, int style) {
super(parent, style);
int osStyle = OS.GetWindowLong(handle, OS.GWL_STYLE);
osStyle |= OS.BS_OWNERDRAW;
OS.SetWindowLong(handle, OS.GWL_STYLE, osStyle);
}
LRESULT wmDrawChild(int wParam, int lParam) {
super.wmDrawChild(wParam, lParam); // 保留箭頭按鈕預設行為
DRAWITEMSTRUCT struct = new DRAWITEMSTRUCT();
OS.MoveMemory(struct, lParam, DRAWITEMSTRUCT.sizeof);
ownerDraw(struct);
return null;
}
protected abstract void ownerDraw(DRAWITEMSTRUCT dis);
}
構造函數注入 BS_OWNERDRAW 風格;wmDrawChild 解析 DRAWITEMSTRUCT 並委派給抽象方法 ownerDraw,子類只需專注繪製邏輯。
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.internal.win32.*;
import org.eclipse.swt.widgets.*;
public class TestButton extends OwnerDrawButton {
TestButton(Composite parent) {
super(parent, SWT.PUSH);
}
@Override
protected void ownerDraw(DRAWITEMSTRUCT dis) {
Rectangle rc = new Rectangle(dis.left, dis.top, dis.right - dis.left, dis.bottom - dis.top);
Color clr1 = new Color(getDisplay(), 0, 255, 128);
Color clr2 = new Color(getDisplay(), 0, 128, 255);
fillGradientRectangle(dis.hDC, rc, true, clr1, clr2);
clr1.dispose(); clr2.dispose();
SIZE size = new SIZE();
String text = getText();
char[] chars = text.toCharArray();
int oldFont = OS.SelectObject(dis.hDC, getFont().handle);
OS.GetTextExtentPoint32W(dis.hDC, chars, chars.length, size);
RECT rcText = new RECT();
rcText.left = rc.x; rcText.top = rc.y;
rcText.right = rc.x + rc.width; rcText.bottom = rc.y + rc.height;
if ((dis.itemState & OS.ODS_SELECTED) != 0)
OS.OffsetRect(rcText, 1, 1); // 按下效果:文字位移 1px
OS.SetBkMode(dis.hDC, OS.TRANSPARENT);
OS.DrawTextW(dis.hDC, chars, -1, rcText, OS.DT_SINGLELINE | OS.DT_CENTER | OS.DT_VCENTER);
OS.SelectObject(dis.hDC, oldFont);
}
private void fillGradientRectangle(int handle, Rectangle rc, boolean vertical, Color clr1, Color clr2) {
int hHeap = OS.GetProcessHeap();
int pMesh = OS.HeapAlloc(hHeap, OS.HEAP_ZERO_MEMORY, GRADIENT_RECT.sizeof + TRIVERTEX.sizeof * 2);
int pVertex = pMesh + GRADIENT_RECT.sizeof;
GRADIENT_RECT gradientRect = new GRADIENT_RECT();
gradientRect.UpperLeft = 0; gradientRect.LowerRight = 1;
OS.MoveMemory(pMesh, gradientRect, GRADIENT_RECT.sizeof);
TRIVERTEX trivertex = new TRIVERTEX();
trivertex.x = rc.x; trivertex.y = rc.y;
trivertex.Red = (short)(clr1.getRed() << 8);
trivertex.Green = (short)(clr1.getGreen() << 8);
trivertex.Blue = (short)(clr1.getBlue() << 8);
trivertex.Alpha = -1;
OS.MoveMemory(pVertex, trivertex, TRIVERTEX.sizeof);
trivertex.x = rc.x + rc.width; trivertex.y = rc.y + rc.height;
trivertex.Red = (short)(clr2.getRed() << 8);
trivertex.Green = (short)(clr2.getGreen() << 8);
trivertex.Blue = (short)(clr2.getBlue() << 8);
trivertex.Alpha = -1;
OS.MoveMemory(pVertex + TRIVERTEX.sizeof, trivertex, TRIVERTEX.sizeof);
OS.GradientFill(handle, pVertex, 2, pMesh, 1, vertical ? OS.GRADIENT_FILL_RECT_V : OS.GRADIENT_FILL_RECT_H);
OS.HeapFree(hHeap, 0, pMesh);
}
@Override protected void checkSubclass() {} // 繞過 SWT 禁止繼承 Button 的檢查
}
關鍵點:
1. 直接操作 Win32 HDC,使用 GradientFill 繪製漸層。
2. 根據 dis.itemState & ODS_SELECTED 判斷按下狀態,文字位移 1 像素模擬按下效果。
3. 覆寫 checkSubclass() 為空實現,允許繼承 Button。
4. JDK 1.4 環境請移除 @Override 註解。
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.*;
public class Application {
public static void main(String[] args) {
Display display = Display.getDefault();
Shell shell = new Shell(display);
shell.setText("Owner Draw Button Test");
FillLayout layout = new FillLayout();
layout.marginWidth = layout.marginHeight = 8;
shell.setLayout(layout);
Button btn = new TestButton(shell);
btn.setText("Owner Draw Button");
btn.setToolTipText("Hello, I'm a OwnerDraw Button!");
shell.pack(); shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) display.sleep();
}
}
}

org.eclipse.swt.widgets 套件,屬 Hack 手段,升級 SWT 版本需回歸測試GC 風格包裝類簡化繪圖代碼;支援高 DPI 縮放;整合主題引擎適配 Windows 10/11 深色模式此技術為 SWT 開發者打開了原生級 UI 客製化大門,適合對視覺品質要求極高的桌面應用場景。