首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

SWT 進階:基於 Owner-Draw 實現高度自訂按鈕組件

     編輯:Java 相關知識

SWT Button 雖功能豐富,但外觀客製化能力極弱:無法實現漸層背景、特殊文字效果,甚至連邊框都無法移除。嘗試 PaintListener 會在按下瞬間失效;使用圖片則面臨邊框破壞美感、資源浪費、縮放失真等問題;繼承 Canvas 模擬按鈕又需手動維護狀態,且無法融入 JFace Action 體系。本文展示如何利用 Windows 底層 BS_OWNERDRAW 機制,透過繼承 Button 並覆寫 wmDrawChild 實現真正的自繪按鈕,完美保留原生行為與 JFace 整合能力。

核心原理:Windows Owner-Draw 機制

Win32 API 中,帶 BS_OWNERDRAW 風格的按鈕會接收 WM_DRAWITEM 訊息,應用程式可在其中完全控制繪製。SWT Button 內部已在 wmDrawChild 中處理此訊息(僅用於 SWT.ARROW 風格),且該方法為 package-private,故我們需將自訂類置於 org.eclipse.swt.widgets 套件下即可覆寫。

抽象基類:OwnerDrawButton

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();
        }
    }
}

Owner Draw Button 效果圖

優缺點總結與擴展建議

  • ✅ 完美保留原生按鈕行為(焦點、熱鍵、工具提示、JFace Action 整合)
  • ✅ 繪製邏輯完全可控,支援任意複雜視覺效果
  • ⚠️ 依賴 Windows 平台與 SWT 內部 API,跨平台需條件編譯或抽象層
  • ⚠️ 需置於 org.eclipse.swt.widgets 套件,屬 Hack 手段,升級 SWT 版本需回歸測試
  • 💡 進階:可封裝 GC 風格包裝類簡化繪圖代碼;支援高 DPI 縮放;整合主題引擎適配 Windows 10/11 深色模式

此技術為 SWT 開發者打開了原生級 UI 客製化大門,適合對視覺品質要求極高的桌面應用場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved