[feat] separate Windows taskbar groups for main and remote control windows, refs #148

This commit is contained in:
dijunkun
2026-11-11 00:04:40 +08:00
parent 57c569424e
commit ed58172d2e
6 changed files with 105 additions and 0 deletions
@@ -62,6 +62,7 @@
#include "platform/windows/gui/shutdown_session_guard.h" #include "platform/windows/gui/shutdown_session_guard.h"
#include "platform/windows/gui/tray/win_tray.h" #include "platform/windows/gui/tray/win_tray.h"
#include "platform/windows/gui/window_icons.h" #include "platform/windows/gui/window_icons.h"
#include "platform/windows/gui/window_taskbar.h"
#include "platform/windows/service/autostart_control.h" #include "platform/windows/service/autostart_control.h"
#include "platform/windows/service/interactive_desktop.h" #include "platform/windows/service/interactive_desktop.h"
#include "platform/windows/service/unattended_config.h" #include "platform/windows/service/unattended_config.h"
@@ -3882,6 +3883,11 @@ void GuiApplication::SyncStreamWindow() {
if (!ui_->stream) { if (!ui_->stream) {
return; return;
} }
// Assign the window's taskbar group before its first show or restoration.
// Reapply when Slint recreates the HWND; the helper skips configured handles.
#if _WIN32
ConfigureWindowsStreamTaskbar((*ui_->stream)->window().win32_hwnd());
#endif
// A wall takeover hides an existing stream window without destroying it. // A wall takeover hides an existing stream window without destroying it.
// Opening a recent device must also restore that existing window. // Opening a recent device must also restore that existing window.
if (need_to_create_stream_window_) { if (need_to_create_stream_window_) {
@@ -0,0 +1,79 @@
#include "window_taskbar.h"
#include <commctrl.h>
#include <propkey.h>
#include <propsys.h>
#include <propvarutil.h>
#include <shellapi.h>
#include <wrl/client.h>
namespace crossdesk {
namespace {
constexpr wchar_t kStreamAppId[] = L"CrossDesk.RemoteControl";
constexpr UINT_PTR kTaskbarSubclassId = 1;
void ClearTaskbarProperties(IPropertyStore* properties) {
const PROPVARIANT empty{};
properties->SetValue(PKEY_AppUserModel_ID, empty);
properties->SetValue(PKEY_AppUserModel_PreventPinning, empty);
}
LRESULT CALLBACK TaskbarSubclassProc(HWND window, UINT message, WPARAM wparam,
LPARAM lparam, UINT_PTR subclass_id,
DWORD_PTR reference_data) {
auto* properties = reinterpret_cast<IPropertyStore*>(reference_data);
if (message == WM_DESTROY) {
// Window property stores require explicit clearing before destruction.
ClearTaskbarProperties(properties);
}
if (message == WM_NCDESTROY) {
RemoveWindowSubclass(window, TaskbarSubclassProc, subclass_id);
properties->Release();
}
return DefSubclassProc(window, message, wparam, lparam);
}
} // namespace
bool ConfigureWindowsStreamTaskbar(HWND window) {
if (!window || !IsWindow(window)) return false;
DWORD_PTR existing = 0;
if (GetWindowSubclass(window, TaskbarSubclassProc, kTaskbarSubclassId,
&existing)) {
return true;
}
Microsoft::WRL::ComPtr<IPropertyStore> properties;
if (FAILED(SHGetPropertyStoreForWindow(window, IID_PPV_ARGS(&properties)))) {
return false;
}
if (!SetWindowSubclass(window, TaskbarSubclassProc, kTaskbarSubclassId,
reinterpret_cast<DWORD_PTR>(properties.Get()))) {
return false;
}
// A live session cannot be recreated by launching the executable. Keep
// pinning on the main application's button instead of creating a shortcut
// that claims to reopen a remote session but only opens the connection page.
PROPVARIANT prevent_pinning{};
InitPropVariantFromBoolean(TRUE, &prevent_pinning);
HRESULT result = properties->SetValue(PKEY_AppUserModel_PreventPinning,
prevent_pinning);
PROPVARIANT app_id{};
if (SUCCEEDED(result)) result = InitPropVariantFromString(kStreamAppId, &app_id);
if (SUCCEEDED(result)) result = properties->SetValue(PKEY_AppUserModel_ID, app_id);
PropVariantClear(&app_id);
if (FAILED(result)) {
ClearTaskbarProperties(properties.Get());
RemoveWindowSubclass(window, TaskbarSubclassProc, kTaskbarSubclassId);
return false;
}
// SetValue applies immediately; this store does not require Commit(). The
// subclass retains the store until this HWND is destroyed, even if Slint
// later creates a new native window for the same component.
properties.Detach();
return true;
}
} // namespace crossdesk
@@ -0,0 +1,15 @@
#ifndef CROSSDESK_WINDOWS_WINDOW_TASKBAR_H_
#define CROSSDESK_WINDOWS_WINDOW_TASKBAR_H_
#include <windows.h>
namespace crossdesk {
// Give the remote control window its own taskbar group, leaving the main
// window's shortcut grouping intact. Call on the UI thread before showing a
// native window; repeated calls are harmless, including after hide/show.
bool ConfigureWindowsStreamTaskbar(HWND window);
} // namespace crossdesk
#endif // CROSSDESK_WINDOWS_WINDOW_TASKBAR_H_
+1
View File
@@ -686,6 +686,7 @@ function setup_targets()
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp", add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
"apps/desktop/src/platform/windows/gui/slint_backend.cpp", "apps/desktop/src/platform/windows/gui/slint_backend.cpp",
"apps/desktop/src/platform/windows/gui/window_icons.cpp", "apps/desktop/src/platform/windows/gui/window_icons.cpp",
"apps/desktop/src/platform/windows/gui/window_taskbar.cpp",
"apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp", "apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp",
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp", "apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp", "apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
+2
View File
@@ -127,6 +127,8 @@ sudo apt install "./crossdesk-virtual-desktop-xfce-linux-amd64-<version>.deb"
桌面端可同步文本剪贴板。接收文件的位置可在“设置 → 常规 → 文件保存路径”调整。关闭主窗口会隐藏窗口并保持程序运行;需要完全退出时使用系统托盘或菜单栏中的退出入口。 桌面端可同步文本剪贴板。接收文件的位置可在“设置 → 常规 → 文件保存路径”调整。关闭主窗口会隐藏窗口并保持程序运行;需要完全退出时使用系统托盘或菜单栏中的退出入口。
Windows 任务栏将主窗口与远程控制窗口显示为两个独立图标,可分别切换和最小化。关闭主窗口后,远程控制窗口及其连接继续保留;需要主窗口时从系统托盘重新打开。远程控制窗口的任务栏图标仅在会话窗口存在时显示,固定应用请使用主窗口的图标。
桌面端普通会话、桌面墙和移动端(Android、iOS、鸿蒙)的网络状态均显示“被控端编码器”和“控制端解码器”,使用实际的视频编解码器名称,包括回退后的软件编解码器。名称保持单行,超出可用宽度时自动横向滚动,完整显示时保持静止。名称统一采用“实现名称 (编码格式)”,名称首字母大写、技术缩写保留全大写,例如 `VideoToolbox (H.264)`、`NVENC (H.264)`、`NVDEC (H.264)`、`SVT-AV1 (AV1)`、`Dav1d (AV1)` 和 `LibAOM (AV1)`;Media Foundation 额外标注 `HW`(硬件)或 `SW`(软件)。被控端需要支持编码器状态上报;旧版本或尚未获取到的信息显示“—”。 桌面端普通会话、桌面墙和移动端(Android、iOS、鸿蒙)的网络状态均显示“被控端编码器”和“控制端解码器”,使用实际的视频编解码器名称,包括回退后的软件编解码器。名称保持单行,超出可用宽度时自动横向滚动,完整显示时保持静止。名称统一采用“实现名称 (编码格式)”,名称首字母大写、技术缩写保留全大写,例如 `VideoToolbox (H.264)`、`NVENC (H.264)`、`NVDEC (H.264)`、`SVT-AV1 (AV1)`、`Dav1d (AV1)` 和 `LibAOM (AV1)`;Media Foundation 额外标注 `HW`(硬件)或 `SW`(软件)。被控端需要支持编码器状态上报;旧版本或尚未获取到的信息显示“—”。
### 在会话中调整画面 ### 在会话中调整画面
+2
View File
@@ -127,6 +127,8 @@ Switch devices using session tabs. Expand the control bar if it is collapsed, an
Desktop clients synchronize text clipboard contents. Configure the receiving directory under **Settings → General → File Save Path**. Closing the main window hides it and keeps the app running; use the system tray or menu-bar exit command to quit completely. Desktop clients synchronize text clipboard contents. Configure the receiving directory under **Settings → General → File Save Path**. Closing the main window hides it and keeps the app running; use the system tray or menu-bar exit command to quit completely.
On Windows, the main window and remote control window use separate taskbar buttons, so you can switch to or minimize either window independently. Closing the main window leaves the remote control window and its connections running; reopen the main window from the system tray. The remote control button appears only while the session window is present; use the main window's button to pin the application.
Network statistics in both ordinary sessions and DeskHub show the video codecs actually in use, including software fallback. Names use “Implementation (Format)” with an initial capital and uppercase technical abbreviations, such as `VideoToolbox (H.264)`, `NVENC (H.264)`, `NVDEC (H.264)`, `SVT-AV1 (AV1)`, `Dav1d (AV1)`, and `LibAOM (AV1)`; Media Foundation also includes `HW` (hardware) or `SW` (software). The host must support encoder status reporting; older versions and information not yet available display “—”. Network statistics in both ordinary sessions and DeskHub show the video codecs actually in use, including software fallback. Names use “Implementation (Format)” with an initial capital and uppercase technical abbreviations, such as `VideoToolbox (H.264)`, `NVENC (H.264)`, `NVDEC (H.264)`, `SVT-AV1 (AV1)`, `Dav1d (AV1)`, and `LibAOM (AV1)`; Media Foundation also includes `HW` (hardware) or `SW` (software). The host must support encoder status reporting; older versions and information not yet available display “—”.
### Adjust video during a session ### Adjust video during a session