feat: 初步实现自动隐藏光标

This commit is contained in:
Xu 2025-08-23 16:50:42 +08:00
commit 56cf990fa7
18 changed files with 236 additions and 57 deletions

View file

@ -86,16 +86,48 @@ bool CursorDrawer::Initialize(DeviceResources& deviceResources) noexcept {
}
void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept {
using namespace std::chrono;
if (!_isCursorVisible) {
// 截屏时暂时不渲染光标
return;
}
const CursorManager& cursorManager = ScalingWindow::Get().CursorManager();
const ScalingWindow& scalingWindow = ScalingWindow::Get();
const ScalingOptions& options = ScalingWindow::Get().Options();
const CursorManager& cursorManager = scalingWindow.CursorManager();
const HCURSOR hCursor = cursorManager.CursorHandle();
POINT cursorPos = cursorManager.CursorPos();
// 转换为渲染矩形局部坐标
const RECT& rendererRect = ScalingWindow::Get().RendererRect();
cursorPos.x -= rendererRect.left;
cursorPos.y -= rendererRect.top;
_lastCursorHandle = NULL;
if (options.autoHideCursorDelay.has_value()) {
if (cursorManager.IsCursorCaptured() &&
!scalingWindow.IsResizingOrMoving() &&
!scalingWindow.SrcTracker().IsMoving() &&
_lastCursorPos == cursorPos &&
(_lastCursorHandle == hCursor || !hCursor))
{
const auto now = steady_clock::now();
if (_lastCursorActiveTime == steady_clock::time_point{}) {
_lastCursorActiveTime = now;
} else {
const duration<float> hideDelay(*options.autoHideCursorDelay);
if (now - _lastCursorActiveTime > hideDelay) {
_lastCursorHandle = hCursor;
return;
}
}
} else {
_lastCursorActiveTime = steady_clock::time_point{};
}
}
_lastCursorHandle = hCursor;
_lastCursorPos = cursorPos;
if (!hCursor) {
return;
@ -106,15 +138,6 @@ void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept
return;
}
// 转换为渲染矩形局部坐标
const RECT& rendererRect = ScalingWindow::Get().RendererRect();
cursorPos.x -= rendererRect.left;
cursorPos.y -= rendererRect.top;
_lastCursorHandle = hCursor;
_lastCursorPos = cursorPos;
const ScalingOptions& options = ScalingWindow::Get().Options();
float cursorScaling = options.cursorScaling;
if (cursorScaling < FLOAT_EPSILON<float>) {
// 光标缩放和源窗口相同

View file

@ -72,6 +72,7 @@ private:
HCURSOR _lastCursorHandle = NULL;
POINT _lastCursorPos{ std::numeric_limits<LONG>::max(), std::numeric_limits<LONG>::max() };
std::chrono::steady_clock::time_point _lastCursorActiveTime;
bool _isCursorVisible = true;
};

View file

@ -328,6 +328,17 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
void ScalingWindow::Start(HWND hwndSrc, ScalingOptions&& options) noexcept {
assert(!Handle());
assert(!options.effects.empty());
assert(options.cropping.Left >= 0 && options.cropping.Top >= 0 &&
options.cropping.Right >= 0 && options.cropping.Bottom >= 0);
assert(options.minFrameRate >= 0);
assert(!options.maxFrameRate.has_value() || *options.maxFrameRate > 0);
assert(options.cursorScaling >= 0);
assert(!options.autoHideCursorDelay.has_value() || *options.autoHideCursorDelay > 0);
assert(options.initialWindowedScaleFactor >= 0);
assert(!options.screenshotsDir.empty());
assert(options.showToast && options.showError && options.save);
options.Log();
// 缩放结束后失效
_options = std::move(options);
@ -701,12 +712,14 @@ LRESULT ScalingWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) n
// 阻止 OS 修改置顶状态。当源窗口中途置顶/取消置顶时OS 会试图修改缩放窗口的置顶
// 状态,这不是我们想要的。
if (!(windowPos.flags & SWP_NOZORDER)) {
if (_srcTracker.IsFocused() || IsTopmostWindow(_srcTracker.Handle())) {
if (windowPos.hwndInsertAfter != HWND_TOP) {
windowPos.hwndInsertAfter = HWND_TOPMOST;
if (!_options.IsDebugMode()) {
if (_srcTracker.IsFocused() || IsTopmostWindow(_srcTracker.Handle())) {
if (windowPos.hwndInsertAfter != HWND_TOP) {
windowPos.hwndInsertAfter = HWND_TOPMOST;
}
} else if (windowPos.hwndInsertAfter == HWND_TOPMOST) {
windowPos.hwndInsertAfter = HWND_NOTOPMOST;
}
} else if (windowPos.hwndInsertAfter == HWND_TOPMOST) {
windowPos.hwndInsertAfter = HWND_NOTOPMOST;
}
// 缩放窗口置顶或取消置顶时避免影响源窗口的 Z 顺序。理论上不需要这个标志,但消息

View file

@ -48,7 +48,11 @@ public:
return _options;
}
SrcTracker& SrcTracker() noexcept {
class SrcTracker& SrcTracker() noexcept {
return _srcTracker;
}
const class SrcTracker& SrcTracker() const noexcept {
return _srcTracker;
}
@ -56,7 +60,15 @@ public:
return *_renderer;
}
CursorManager& CursorManager() noexcept {
const class Renderer& Renderer() const noexcept {
return *_renderer;
}
class CursorManager& CursorManager() noexcept {
return *_cursorManager;
}
const class CursorManager& CursorManager() const noexcept {
return *_cursorManager;
}

View file

@ -184,6 +184,7 @@ struct ScalingOptions {
CaptureMethod captureMethod = CaptureMethod::GraphicsCapture;
MultiMonitorUsage multiMonitorUsage = MultiMonitorUsage::Closest;
CursorInterpolationMode cursorInterpolationMode = CursorInterpolationMode::NearestNeighbor;
std::optional<float> autoHideCursorDelay;
DuplicateFrameDetectionMode duplicateFrameDetectionMode = DuplicateFrameDetectionMode::Dynamic;
ToolbarState fullscreenInitialToolbarState = ToolbarState::AutoHide;
ToolbarState windowedInitialToolbarState = ToolbarState::AutoHide;

View file

@ -121,6 +121,10 @@ static void WriteProfile(rapidjson::PrettyWriter<rapidjson::StringBuffer>& write
writer.Double(profile.customCursorScaling);
writer.Key("cursorInterpolationMode");
writer.Uint((uint32_t)profile.cursorInterpolationMode);
writer.Key("autoHideCursorEnabled");
writer.Bool(profile.isAutoHideCursorEnabled);
writer.Key("autoHideCursorDelay");
writer.Double(profile.autoHideCursorDelay);
writer.Key("croppingEnabled");
writer.Bool(profile.isCroppingEnabled);
@ -1060,7 +1064,9 @@ bool AppSettings::_LoadProfile(
JsonHelper::ReadBool(profileObj, "frameRateLimiterEnabled", profile.isFrameRateLimiterEnabled);
JsonHelper::ReadFloat(profileObj, "maxFrameRate", profile.maxFrameRate);
if (profile.maxFrameRate < 10.0f || profile.maxFrameRate > 1000.0f) {
if (profile.maxFrameRate <= 10.0f - FLOAT_EPSILON<float> ||
profile.maxFrameRate >= 1000.0f + FLOAT_EPSILON<float>)
{
profile.maxFrameRate = 60.0f;
}
@ -1095,6 +1101,14 @@ bool AppSettings::_LoadProfile(
profile.cursorInterpolationMode = (CursorInterpolationMode)cursorInterpolationMode;
}
JsonHelper::ReadBool(profileObj, "autoHideCursorEnabled", profile.isAutoHideCursorEnabled);
JsonHelper::ReadFloat(profileObj, "autoHideCursorDelay", profile.autoHideCursorDelay);
if (profile.autoHideCursorDelay <= 0.1f - FLOAT_EPSILON<float> ||
profile.autoHideCursorDelay >= 5.0f + FLOAT_EPSILON<float>)
{
profile.autoHideCursorDelay = 3.0f;
}
JsonHelper::ReadBool(profileObj, "croppingEnabled", profile.isCroppingEnabled);
auto croppingNode = profileObj.FindMember("cropping");

View file

@ -105,11 +105,24 @@
<local:SettingsExpander.Items>
<local:SettingsCard x:Uid="Home_Activation_Timer_Delay"
IsWrapEnabled="True">
<Slider Loaded="TimerSlider_Loaded"
Maximum="5"
Minimum="1"
TickFrequency="1"
Value="{x:Bind ViewModel.Delay, Mode=TwoWay}" />
<Grid MinWidth="{StaticResource SettingsCardContentMinWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Slider Grid.Column="0"
MinWidth="0"
IsThumbToolTipEnabled="False"
Loaded="TimerSlider_Loaded"
Maximum="5"
Minimum="1"
Value="{x:Bind ViewModel.Delay, Mode=TwoWay}" />
<TextBlock Grid.Column="1"
Width="20"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.DelayText, Mode=OneWay}"
TextAlignment="Right" />
</Grid>
</local:SettingsCard>
</local:SettingsExpander.Items>
</local:SettingsExpander>

View file

@ -95,10 +95,15 @@ uint32_t HomeViewModel::Delay() const noexcept {
void HomeViewModel::Delay(uint32_t value) {
AppSettings::Get().CountdownSeconds(value);
RaisePropertyChanged(L"Delay");
RaisePropertyChanged(L"DelayText");
RaisePropertyChanged(L"TimerDescription");
}
inline void HomeViewModel::ShowUpdateCard(bool value) noexcept {
hstring HomeViewModel::DelayText() const noexcept {
return App::Get().DoubleFormatter().FormatDouble(Delay());
}
void HomeViewModel::ShowUpdateCard(bool value) noexcept {
_showUpdateCard = value;
if (!value) {
UpdateService::Get().IsShowOnHomePage(false);

View file

@ -15,10 +15,6 @@ struct HomeViewModel : HomeViewModelT<HomeViewModel>, wil::notify_property_chang
hstring TimerLabelText() const noexcept;
hstring TimerFullscreenButtonText() const noexcept;
hstring TimerWindowedButtonText() const noexcept;
bool IsNotRunning() const noexcept;
hstring TimerButtonText(bool windowedMode) const noexcept;
@ -30,6 +26,8 @@ struct HomeViewModel : HomeViewModelT<HomeViewModel>, wil::notify_property_chang
uint32_t Delay() const noexcept;
void Delay(uint32_t value);
hstring DelayText() const noexcept;
bool ShowUpdateCard() const noexcept {
return _showUpdateCard;
}

View file

@ -13,6 +13,7 @@ namespace Magpie {
String TimerLabelText { get; };
Boolean IsNotRunning { get; };
UInt32 Delay;
String DelayText { get; };
String TimerButtonText(Boolean windowedMode);
void ToggleTimerFullscreen();

View file

@ -77,6 +77,9 @@ struct Profile {
CursorScaling cursorScaling = CursorScaling::NoScaling;
float customCursorScaling = 1.0;
// 0.1~5
float autoHideCursorDelay = 3.0f;
Cropping cropping{};
// -1 表示原样
int scalingMode = -1;
@ -95,6 +98,7 @@ struct Profile {
bool isPackaged = false;
bool isCroppingEnabled = false;
bool isFrameRateLimiterEnabled = false;
bool isAutoHideCursorEnabled = false;
};
}

View file

@ -470,6 +470,40 @@
</local:SettingsCard>
</local:SettingsExpander.Items>
</local:SettingsExpander>
<local:SettingsExpander x:Uid="Profile_Cursor_AutoHide"
IsExpanded="{x:Bind ViewModel.IsAutoHideCursorEnabled, Mode=OneWay}">
<local:SettingsExpander.HeaderIcon>
<FontIcon Glyph="&#xEC32;" />
</local:SettingsExpander.HeaderIcon>
<local:SettingsExpander.Content>
<ToggleSwitch x:Uid="ToggleSwitch"
IsOn="{x:Bind ViewModel.IsAutoHideCursorEnabled, Mode=TwoWay}" />
</local:SettingsExpander.Content>
<local:SettingsExpander.Items>
<local:SettingsCard x:Uid="Profile_Cursor_AutoHide_Delay"
IsWrapEnabled="True">
<Grid MinWidth="{StaticResource SettingsCardContentMinWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Slider Grid.Column="0"
MinWidth="0"
VerticalAlignment="Center"
IsThumbToolTipEnabled="False"
Maximum="5"
Minimum="0.1"
StepFrequency="0.1"
Value="{x:Bind ViewModel.AutoHideCursorDelay, Mode=TwoWay}" />
<TextBlock Grid.Column="1"
Width="18"
Margin="8,0,0,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.AutoHideCursorDelayText, Mode=OneWay}" />
</Grid>
</local:SettingsCard>
</local:SettingsExpander.Items>
</local:SettingsExpander>
<local:SettingsCard x:Uid="Profile_Cursor_DrawCursor_AdjustCursorSpeed">
<local:SettingsCard.HeaderIcon>
<PathIcon Data="{StaticResource MoveIconData}" />

View file

@ -746,6 +746,41 @@ void ProfileViewModel::CursorInterpolationMode(int value) {
RaisePropertyChanged(L"CursorInterpolationMode");
}
bool ProfileViewModel::IsAutoHideCursorEnabled() const noexcept {
return _data->isAutoHideCursorEnabled;
}
void ProfileViewModel::IsAutoHideCursorEnabled(bool value) {
if (_data->isAutoHideCursorEnabled == value) {
return;
}
_data->isAutoHideCursorEnabled = value;
AppSettings::Get().SaveAsync();
RaisePropertyChanged(L"IsAutoHideCursorEnabled");
}
double ProfileViewModel::AutoHideCursorDelay() const noexcept {
return _data->autoHideCursorDelay;
}
void ProfileViewModel::AutoHideCursorDelay(double value) {
if (_data->autoHideCursorDelay == value) {
return;
}
_data->autoHideCursorDelay = std::isnan(value) ? 3.0f : (float)value;
AppSettings::Get().SaveAsync();
RaisePropertyChanged(L"AutoHideCursorDelay");
RaisePropertyChanged(L"AutoHideCursorDelayText");
}
hstring ProfileViewModel::AutoHideCursorDelayText() const noexcept {
return App::Get().DoubleFormatter().FormatDouble(AutoHideCursorDelay());
}
hstring ProfileViewModel::LaunchParameters() const noexcept {
return hstring(_data->launchParameters);
}

View file

@ -131,6 +131,14 @@ struct ProfileViewModel : ProfileViewModelT<ProfileViewModel>,
int CursorInterpolationMode() const noexcept;
void CursorInterpolationMode(int value);
bool IsAutoHideCursorEnabled() const noexcept;
void IsAutoHideCursorEnabled(bool value);
double AutoHideCursorDelay() const noexcept;
void AutoHideCursorDelay(double value);
hstring AutoHideCursorDelayText() const noexcept;
hstring LaunchParameters() const noexcept;
void LaunchParameters(const hstring& value);

View file

@ -60,6 +60,9 @@ namespace Magpie {
Int32 CursorScaling;
Double CustomCursorScaling;
Int32 CursorInterpolationMode;
Boolean IsAutoHideCursorEnabled;
Double AutoHideCursorDelay;
String AutoHideCursorDelayText { get; };
String LaunchParameters;
Boolean IsDirectFlipDisabled;

View file

@ -1012,4 +1012,10 @@
<data name="Overlay_Toolbar_Minimize" xml:space="preserve">
<value>Minimize</value>
</data>
<data name="Profile_Cursor_AutoHide.Header" xml:space="preserve">
<value>Auto-hide cursor when idle</value>
</data>
<data name="Profile_Cursor_AutoHide_Delay.Header" xml:space="preserve">
<value>Hide delay in seconds</value>
</data>
</root>

View file

@ -133,7 +133,7 @@
<value>取消</value>
</data>
<data name="Home_Activation_Timer_Delay.Header" xml:space="preserve">
<value>倒计时时长</value>
<value>倒计时时长(秒)</value>
</data>
<data name="ShortcutDialog_Cancel" xml:space="preserve">
<value>取消</value>
@ -1012,4 +1012,10 @@
<data name="Overlay_Toolbar_Minimize" xml:space="preserve">
<value>最小化</value>
</data>
<data name="Profile_Cursor_AutoHide.Header" xml:space="preserve">
<value>光标静止时自动隐藏</value>
</data>
<data name="Profile_Cursor_AutoHide_Delay.Header" xml:space="preserve">
<value>隐藏延迟(秒)</value>
</data>
</root>

View file

@ -350,33 +350,31 @@ ScalingError ScalingService::_StartScaleImpl(HWND hWnd, const Profile& profile,
options.IsWindowedMode(windowedMode);
options.IsTouchSupportEnabled(isTouchSupportEnabled);
if (windowedMode) {
switch (profile.initialWindowedScaleFactor) {
case InitialWindowedScaleFactor::Auto:
options.initialWindowedScaleFactor = 0.0f;
break;
case InitialWindowedScaleFactor::x1_25:
options.initialWindowedScaleFactor = 1.25f;
break;
case InitialWindowedScaleFactor::x1_5:
options.initialWindowedScaleFactor = 1.5f;
break;
case InitialWindowedScaleFactor::x1_75:
options.initialWindowedScaleFactor = 1.75f;
break;
case InitialWindowedScaleFactor::x2:
options.initialWindowedScaleFactor = 2.0f;
break;
case InitialWindowedScaleFactor::x3:
options.initialWindowedScaleFactor = 3.0f;
break;
case InitialWindowedScaleFactor::Custom:
options.initialWindowedScaleFactor = profile.customInitialWindowedScaleFactor;
break;
default:
options.initialWindowedScaleFactor = 0.0f;
break;
}
switch (profile.initialWindowedScaleFactor) {
case InitialWindowedScaleFactor::Auto:
options.initialWindowedScaleFactor = 0.0f;
break;
case InitialWindowedScaleFactor::x1_25:
options.initialWindowedScaleFactor = 1.25f;
break;
case InitialWindowedScaleFactor::x1_5:
options.initialWindowedScaleFactor = 1.5f;
break;
case InitialWindowedScaleFactor::x1_75:
options.initialWindowedScaleFactor = 1.75f;
break;
case InitialWindowedScaleFactor::x2:
options.initialWindowedScaleFactor = 2.0f;
break;
case InitialWindowedScaleFactor::x3:
options.initialWindowedScaleFactor = 3.0f;
break;
case InitialWindowedScaleFactor::Custom:
options.initialWindowedScaleFactor = profile.customInitialWindowedScaleFactor;
break;
default:
options.initialWindowedScaleFactor = 0.0f;
break;
}
if (profile.isCroppingEnabled) {
@ -414,6 +412,10 @@ ScalingError ScalingService::_StartScaleImpl(HWND hWnd, const Profile& profile,
break;
}
if (profile.isAutoHideCursorEnabled) {
options.autoHideCursorDelay = profile.autoHideCursorDelay;
}
// 应用全局配置
AppSettings& settings = AppSettings::Get();
options.IsDeveloperMode(settings.IsDeveloperMode());