吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2028|回复: 5
收起左侧

[其他原创] Tauri + Rust 实现 Windows 窗口材质动态切换:从底层 API 到前端交互

  [复制链接]
一只小凡凡 发表于 2025-8-10 00:28

最近在做一个 Tauri 桌面应用时,想加个窗口材质切换功能 —— 就是 Windows 11 里的 Mica(云母)、Acrylic(亚克力) 类半透明效果。本来以为 Tauri 自带相关 API,试了才发现:Tauri 确实能在初始化时设置窗口材质,但运行中动态切换是做不到的。没办法,只能自己动手用 Rust 调用系统 API 实现了,这里把过程分享出来,希望能帮到同样踩坑的朋友。

先说说 Tauri 在材质设置的局限

一开始我查 Tauri 文档,发现可以在 tauri.conf.json 里通过 windows 配置设置 transparent 后,能指定 WindowEffect 来实现材质的更改(还需要设置前端背景为 transparent)。但这些配置都是应用启动时生效的,一旦应用跑起来,就没法通过前端调用修改了。如果用户想在使用中切换不同的窗口效果,原生 API 根本满足不了,所以必须自己写 Rust 后端调用 Windows 的 DWM 接口。

核心思路:用 Rust 对接 Windows DWM API

Windows 的窗口效果是由Desktop Window Manager(DWM)管理的,它提供了 DwmSetWindowAttributeDwmGetWindowAttribute 两个关键 API,分别用来设置和获取窗口属性。我们要做的就是用 Rust 调用这两个 API,实现材质的动态切换。

微软文档链接:

  1. DwmGetWindowAttribute 函数 (dwmapi.h)
  2. DwmSetWindowAttribute 函数 (dwmapi.h)
  3. DWMWINDOWATTRIBUTE 枚举 (dwmapi.h)

第一步:检测系统版本,确定支持的材质

不同 Windows 版本支持的材质不一样,比如 Mica 材质只在 Windows 11(Build 22000+)才支持。所以第一步必须先判断系统版本,避免在不支持的系统上显示无效选项。​
我用 Rust 的 winreg 库(需要 cargo add winreg 使用)读取注册表来获取系统版本:

// 读取注册表中的系统版本号
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let current_version = hklm.open_subkey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")?;
let build_number: String = current_version.get_value("CurrentBuild")?;

拿到build_number后,就能判断哪些材质可用了。比如 Build 22000 以上支持 Mica、Acrylic等,低版本可能只支持默认材质:

// 根据版本动态生成支持的材质列表
if build_number >= 22000 {
    materials.push(MaterialInfo { id: DWMSBT_MAINWINDOW.0, name: "Mica" });
    materials.push(MaterialInfo { id: DWMSBT_TRANSIENTWINDOW.0, name: "Acrylic" });
    materials.push(MaterialInfo { id: DWMSBT_TABBEDWINDOW.0, name: "Tabbed (MicaAlt)" });
}

第二步:获取窗口句柄(HWND)​

调用 DWM API 必须要有窗口句柄(HWND),Tauri 里可以通过 window_handle() 方法获取,但需要注意:Tauri 的窗口可能有多层父窗口,必须找到最顶层的那个句柄才有效。​
我用 Windows API 的 GetParent函数 循环向上查找顶层窗口:

// 从Tauri窗口获取HWND
let window = app.get_webview_window("main")?;
let handle = window.window_handle()?;
// 转换为Windows原生句柄
match handle.as_raw() {
    RawWindowHandle::Win32(h) => {
        let mut current = HWND(isize::from(h.hwnd) as *mut c_void);
        // 循环找顶层窗口
        unsafe {
            loop {
                match GetParent(current) {
                    Ok(parent) if !parent.0.is_null() => current = parent,
                    _ => break,
                }
            }
        }
        Some(current.0 as isize) // 最终拿到可用的HWND
    }
    _ => None
}

这里的 unsafe 块是必须的,因为直接操作系统句柄涉及内存安全,Rust 无法在编译时验证,所以需要开发者自己保证逻辑正确。

第三步:实现材质切换功能​

有了窗口句柄和系统版本信息,就可以调用 DwmSetWindowAttribute 设置材质了。核心代码如下:

// 设置窗口材质
pub fn set_window_material(app: &tauri::AppHandle, material: i32) -> u32 {
    let hwnd = get_window_hwnd(app.clone()).unwrap(); // 获取前面拿到的HWND
    let mut material_id = material; // 材质ID(对应DWM的枚举值)
    unsafe {
        // 调用DWM API设置属性
        DwmSetWindowAttribute(
            HWND(hwnd as *mut c_void), 
            DWMWA_SYSTEMBACKDROP_TYPE, // 要设置的属性:系统背景类型
            (&mut material_id as *mut i32).cast::<c_void>(), // 材质ID的指针
            std::mem::size_of::<i32>() as u32 // 参数大小
        );
    }
    material_id.try_into().unwrap()
}

这里的 material参数 对应 DWM 的枚举值:DWMSBT_AUTO(默认)DWMSBT_MAINWINDOW(Mica)等,前面定义的MaterialInfo结构体就是把这些值和友好名称对应起来,方便前端显示。​

use windows::Win32::Graphics::Dwm::{
    DWMSBT_AUTO,
    DWMSBT_NONE,
    DWMSBT_MAINWINDOW,
    DWMSBT_TRANSIENTWINDOW,
    DWMSBT_TABBEDWINDOW,
    DWMWA_SYSTEMBACKDROP_TYPE,
    DwmGetWindowAttribute,
    DwmSetWindowAttribute
};

#[derive(Serialize)]
pub struct MaterialInfo {
    pub id: i32,
    pub name: &'static str,
}

对于样式样式枚举 DWM_SYSTEMBACKDROP_TYPE 的解释:

  • DWMSBT_AUTO = 0 自动:默认
  • DWMSBT_NONE = 1 无背景样式
  • DWMSBT_MAINWINDOW = 2 云母(Mica)
  • DWMSBT_TRANSIENTWINDOW = 3 亚克力(Acrylic)
  • DWMSBT_TABBEDWINDOW = 4 云母(MicaAlt)

前端交互:Vue 3 如何调用后端功能​

前端部分很简单,主要是用 Tauri 的 invoke函数 调用 Rust 写的命令,然后渲染材质列表和处理选择事件。​前端组件库则使用 shadcn/vue

初始化时加载材质列表​

页面加载时,先调用后端获取支持的材质和当前材质:

// 前端初始化代码
onMounted(() => {
    // 获取支持的材质列表
    invoke('get_supported_window_materials').then(value => {
        materials.value = value as MaterialInfo[];
    });
    // 获取当前正在使用的材质
    invoke('get_current_window_materials').then(value => {
        selectedMaterial.value = (value as MaterialInfo[])[0];
    });
});

用下拉框实现切换交互​

我用了一个 Combobox 组件 展示材质列表,用户选择后调用 set_window_material 命令:

<!-- 前端交互组件 -->
<Combobox>
  <ComboboxTrigger as-child>
    <Button variant="outline">
      {{ selectedMaterial?.name }}
      <ChevronsUpDown class="ml-2 h-4 w-4" />
    </Button>
  </ComboboxTrigger>
  <ComboboxList>
    <ComboboxItem 
      v-for="material in materials" 
      :key="material.id"
      @select="() => setMaterial(material)"
    >
      {{ material.name }}
    </ComboboxItem>
  </ComboboxList>
</Combobox>

// 切换材质的方法
const setMaterial = (material: MaterialInfo) => {
    invoke('set_window_material', { material: material.id })
        .then(() => {
            selectedMaterial.value = material; // 更新选中状态
        })
        .catch(error => {
            console.error('切换失败:', error);
        });
};

遇到的坑

  • 窗口句柄获取错误:一开始直接用了 Tauri 返回的第一层窗口句柄,调用 DWM API 始终失败,后来才发现需要找顶层父窗口,用 GetParent 循环查找才解决,如果不找,则拿到的是 webview 的子窗口句柄。​

最后总结​

虽然 Tauri 原生不支持动态切换窗口材质,但通过 Rust 调用 Windows 的 DWM API 完全可以实现。整个过程的核心就是:正确获取窗口句柄→根据系统版本提供合适的材质选项→用 DWM API 执行切换。​
这个功能写下来最大的感受是:Rust 虽然入门难,但处理系统级 API 时确实安全又高效;而 Tauri 的前后端通信机制也很方便,让前端能轻松调用底层功能。

免费评分

参与人数 8吾爱币 +15 热心值 +7 收起 理由
笙若 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
linllz + 1 用心讨论,共获提升!
Carinx + 1 + 1 用心讨论,共获提升!
vethenc + 1 + 1 谢谢@Thanks!
ZYSJ111666888 + 1 不错哦
rafer1 + 1 我很赞同!
hrh123 + 10 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
WAITME66 + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| 一只小凡凡 发表于 2025-8-10 00:39
本帖最后由 一只小凡凡 于 2025-8-10 00:42 编辑

注意:这里没有实现保存材质状态的功能,需要在窗口初始化阶段通过文件或者数据库来加载用户选择的材质,同时前端在设置材质时应该保存用户的设置,这部分逻辑就不再赘述。

完整代码:

use tauri::Manager;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::GetParent;
use std::ffi::c_void;
use serde::Serialize;
use windows::Win32::Graphics::Dwm::{
    DWMSBT_AUTO,
    DWMSBT_NONE,
    DWMSBT_MAINWINDOW,
    DWMSBT_TRANSIENTWINDOW,
    DWMSBT_TABBEDWINDOW,
    DWMWA_SYSTEMBACKDROP_TYPE,
    DwmGetWindowAttribute,
    DwmSetWindowAttribute
};

pub fn get_system_version() -> u32 {
    #[cfg(windows)]
    {
        use winreg::RegKey;
        use winreg::enums::*;

        let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
        let current_version = hklm.open_subkey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")
            .expect("Failed to open registry key");

        let build_number: String = current_version.get_value("CurrentBuild")
            .expect("Failed to get build number");

        build_number.parse().unwrap_or(0)
    }

    #[cfg(not(windows))]
    (0, 0)
}

#[derive(Serialize)]
pub struct MaterialInfo {
    pub id: i32,
    pub name: &'static str,
}

#[cfg(windows)]
pub fn get_supported_materials() -> Vec<MaterialInfo> {
    let mut materials = vec![MaterialInfo {
        id: DWMSBT_AUTO.0,
        name: "Default",
    }];

    let build_number = get_system_version();

    if build_number >= 22000 {
        materials.push(MaterialInfo { id: DWMSBT_NONE.0, name: "None (No backdrop)" });
        materials.push(MaterialInfo { id: DWMSBT_MAINWINDOW.0, name: "Mica" });
        materials.push(MaterialInfo { id: DWMSBT_TRANSIENTWINDOW.0, name: "Acrylic" });
        materials.push(MaterialInfo { id: DWMSBT_TABBEDWINDOW.0, name: "Tabbed (MicaAlt)" });
    }

    materials
}

#[cfg(windows)]
pub fn get_current_materials(app: &tauri::AppHandle) -> Vec<MaterialInfo> {
    let materials = get_supported_materials();
    let hwnd = get_window_hwnd(app.clone()).unwrap();

    let mut current_materials = vec![];
    for material in materials {
        let mut material_id = 0;
        unsafe {
            let _ = DwmGetWindowAttribute(
                HWND(hwnd as *mut c_void), 
                DWMWA_SYSTEMBACKDROP_TYPE, 
                (&mut material_id as *mut i32).cast::<c_void>(), 
                std::mem::size_of::<i32>() as u32);
        }
        if material_id == material.id {
            current_materials.push(material);
        }
    }
    current_materials
}

#[cfg(windows)]
pub fn set_window_material(app: &tauri::AppHandle, material: i32) -> u32 {
    let hwnd = get_window_hwnd(app.clone()).unwrap();
    let mut material_id = material;
    unsafe {
        let _ = DwmSetWindowAttribute(
            HWND(hwnd as *mut c_void), 
            DWMWA_SYSTEMBACKDROP_TYPE, 
            (&mut material_id as *mut i32).cast::<c_void>(), 
            std::mem::size_of::<i32>() as u32);
    }
    material_id.try_into().unwrap()
}

#[cfg(windows)]
pub fn get_window_hwnd(app: tauri::AppHandle) -> Option<isize> {
    let window = app.get_webview_window("main")?;

    if let Ok(handle) = window.window_handle() {
        match handle.as_raw() {
            RawWindowHandle::Win32(h) => {
                let mut current = HWND(isize::from(h.hwnd) as *mut c_void);
                unsafe {
                    loop {
                        match GetParent(current) {
                            Ok(parent) if !parent.0.is_null() => {
                                current = parent;
                            }
                            _ => break,
                        }
                    }
                }
                return Some(current.0 as isize);
            }
            _ => {}
        }
    }
    None
}
WAITME66 发表于 2025-8-10 02:50
requests111222 发表于 2025-8-10 03:57
saiksy 发表于 2025-8-10 09:50
不错,原来可以这样玩
sinoers 发表于 2025-8-14 09:18
这才是大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-10 07:25

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表