This commit is contained in:
PANDA-JSR
2026-02-04 14:30:27 +08:00
parent 8f6383cbe6
commit 0b0714c166
9 changed files with 269 additions and 25 deletions
+77
View File
@@ -0,0 +1,77 @@
# Ink Canvas 外部协议 (URI Scheme) 说明文档
Ink Canvas 支持通过自定义协议 `icc://` 进行外部调用。通过此功能,其他应用程序、网页脚本或系统快捷方式可以远程控制 Ink Canvas 的运行状态。
## 启用方法
在使用外部协议之前,必须先在软件设置中启用:
1. 打开 **软件设置**
2. 进入 **高级选项** 面板。
3. 找到 **外部协议调用** 区域。
4. 开启 **“启用外部协议 (icc://)”** 开关。
> **注意**:此操作会自动在系统注册表中为当前用户注册协议。如果手动关闭该功能,协议将被注销。
---
## 命令列表
### 1. 基础控制命令
| 命令 | 完整 URI | 作用 |
| :--- | :--- | :--- |
| **Fold** | `icc://fold` | 进入**收纳模式**。如果当前处于展开状态,将清空墨迹并折叠到侧边栏。 |
| **Unfold** | `icc://unfold` | 退出**收纳模式**。如果当前已折叠,将展开浮动工具栏。 |
| **Toggle** | `icc://toggle` | **切换**状态。已展开则收起,已收起则展开。 |
| **Show** | `icc://show` | 与 `unfold` 作用相同,用于兼容旧版指令。 |
### 2. 侧边栏工具命令
以下命令对应收纳模式下侧边栏提供的快速工具。
| 命令 | 完整 URI | 作用 |
| :--- | :--- | :--- |
| **单次抽** | `icc://randone` | 打开随机点名窗口并执行**单次抽选**。 |
| **随机抽** | `icc://rand` | 打开随机点名窗口并执行**随机抽选**。 |
| **计时器** | `icc://timer` | 打开**计时器/倒计时**工具。 |
| **白板** | `icc://whiteboard` | 切换到**白板模式**(也可使用 `icc://board`)。 |
### 3. 进阶功能命令 (隐藏功能)
以下功能专门用于解决与第三方侧边栏或悬浮窗程序的兼容性问题,未在常规设置界面显示。
| 命令 | 完整 URI | 作用 |
| :--- | :--- | :--- |
| **ThoroughHideOn** | `icc://thoroughHideOn` | **开启**“收起时彻底隐藏”功能。开启后,进入收纳模式时主窗口将完全不可见。 |
| **ThoroughHideOff** | `icc://thoroughHideOff` | **关闭**“收起时彻底隐藏”功能。恢复默认的侧边栏边缘留痕模式。 |
| **ThoroughHideToggle** | `icc://thoroughhidetoggle` | **切换**“收起时彻底隐藏”功能的开启/关闭状态。 |
---
## 使用示例
### A. 在浏览器中调用
可以直接在浏览器地址栏输入并回车,或在 HTML 中使用超链接:
```html
<a href="icc://fold">立即收纳 Ink Canvas</a>
```
### B. 在 Windows “运行”对话框中使用
按下 `Win + R`,输入 `icc://toggle` 并回车。
### C. 在批处理或命令行中使用
```cmd
start icc://unfold
```
---
## 开发者说明
### 运行机制
1. **唤醒启动**:如果 Ink Canvas 尚未运行,调用 URI 会直接启动程序并执行命令。
2. **进程间通信 (IPC)**:如果程序已经在运行,外部调用会启动一个临时的指令传递进程,通过系统事件和临时文件将指令发送给已运行的实例,实现无缝控制。
### 兼容性
* 支持 Windows 7 及更高版本。
* 注册表位置:`HKEY_CURRENT_USER\Software\Classes\icc` (无需管理员权限)。
+32 -2
View File
@@ -75,8 +75,38 @@ namespace Ink_Canvas.Helpers
{
if (shellKey == null) return false;
string command = shellKey.GetValue("") as string;
string exePath = Process.GetCurrentProcess().MainModule.FileName;
return !string.IsNullOrEmpty(command) && command.Contains(exePath);
if (string.IsNullOrEmpty(command)) return false;
// 提取第一个标记作为可执行文件路径(处理带引号的情况)
string registeredExePath = "";
if (command.StartsWith("\""))
{
int nextQuote = command.IndexOf("\"", 1);
if (nextQuote > 1)
{
registeredExePath = command.Substring(1, nextQuote - 1);
}
}
else
{
int firstSpace = command.IndexOf(" ");
registeredExePath = firstSpace > 0 ? command.Substring(0, firstSpace) : command;
}
if (string.IsNullOrEmpty(registeredExePath)) return false;
string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
try
{
string normalizedRegisteredPath = System.IO.Path.GetFullPath(registeredExePath);
string normalizedCurrentPath = System.IO.Path.GetFullPath(currentExePath);
return string.Equals(normalizedRegisteredPath, normalizedCurrentPath, StringComparison.OrdinalIgnoreCase);
}
catch
{
return string.Equals(registeredExePath, currentExePath, StringComparison.OrdinalIgnoreCase);
}
}
}
}
+3 -1
View File
@@ -2417,8 +2417,10 @@
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchIsSpecialScreen"
IsOn="True" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchIsSpecialScreen_OnToggled" />
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchIsEnableUriScheme"
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchIsEnableUriScheme"
Visibility="Collapsed" IsOn="False"
AutomationProperties.Name="外部协议调用 (icc://)"
ToolTip="通过 icc:// 协议从外部控制软件"
Toggled="ToggleSwitchIsEnableUriScheme_Toggled" />
</ui:SimpleStackPanel>
<StackPanel Orientation="Vertical">
+18
View File
@@ -105,6 +105,15 @@ namespace Ink_Canvas
HideSubPanels("cursor");
SidePannelMarginAnimation(-10);
});
// 新增:如果开启了彻底隐藏,则隐藏主窗口
if (Settings.Automation.ThoroughlyHideWhenFolded)
{
await Dispatcher.InvokeAsync(() =>
{
this.Visibility = Visibility.Hidden;
});
}
}
private async void LeftUnFoldButtonDisplayQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
@@ -230,6 +239,15 @@ namespace Ink_Canvas
public async Task UnFoldFloatingBar(object sender)
{
// 新增:如果之前彻底隐藏了,先恢复显示
if (this.Visibility != Visibility.Visible)
{
await Dispatcher.InvokeAsync(() =>
{
this.Visibility = Visibility.Visible;
});
}
await Dispatcher.InvokeAsync(() =>
{
LeftUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
+35 -4
View File
@@ -2856,25 +2856,56 @@ namespace Ink_Canvas
private void ToggleSwitchIsEnableUriScheme_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Advanced.IsEnableUriScheme = ToggleSwitchIsEnableUriScheme.IsOn;
if (Settings.Advanced.IsEnableUriScheme)
bool newState = ToggleSwitchIsEnableUriScheme.IsOn;
bool success = false;
try
{
if (newState)
{
if (!UriSchemeHelper.IsUriSchemeRegistered())
{
UriSchemeHelper.RegisterUriScheme();
success = UriSchemeHelper.RegisterUriScheme();
}
else
{
success = true;
}
}
else
{
if (UriSchemeHelper.IsUriSchemeRegistered())
{
UriSchemeHelper.UnregisterUriScheme();
success = UriSchemeHelper.UnregisterUriScheme();
}
else
{
success = true;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"切换URI Scheme状态失败: {ex.Message}", LogHelper.LogType.Error);
success = false;
}
if (success)
{
Settings.Advanced.IsEnableUriScheme = newState;
SaveSettingsToFile();
}
else
{
// 回滚 UI 状态
isLoaded = false;
ToggleSwitchIsEnableUriScheme.IsOn = !newState;
isLoaded = true;
ShowNotification("设置外部协议失败,请检查权限或日志");
}
}
private void TouchMultiplierSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
+61 -1
View File
@@ -12,6 +12,13 @@ namespace Ink_Canvas
{
if (string.IsNullOrEmpty(uri)) return;
// 检查是否启用了外部协议
if (!Settings.Advanced.IsEnableUriScheme)
{
LogHelper.WriteLogToFile($"URI协议已禁用,忽略请求: {uri}", LogHelper.LogType.Warning);
return;
}
LogHelper.WriteLogToFile($"正在处理URI命令: {uri}", LogHelper.LogType.Event);
// 解析URI
@@ -22,8 +29,15 @@ namespace Ink_Canvas
if (Uri.TryCreate(uri, UriKind.Absolute, out Uri uriObj))
{
command = uriObj.Host.ToLower();
// 处理像 icc:fold 这样 Host 可能为空的情况
if (string.IsNullOrEmpty(command))
{
command = uriObj.AbsolutePath.Trim('/').ToLower();
}
else if (uri.StartsWith("icc:", StringComparison.OrdinalIgnoreCase))
}
// 如果解析失败且是 icc: 协议,则手动处理
if (string.IsNullOrEmpty(command) && uri.StartsWith("icc:", StringComparison.OrdinalIgnoreCase))
{
// 简单的手动解析: icc:fold
string path = uri.Substring(4);
@@ -63,6 +77,52 @@ namespace Ink_Canvas
}
break;
case "thoroughhideon":
Settings.Automation.ThoroughlyHideWhenFolded = true;
SaveSettingsToFile();
ShowNotification("已开启:收起时彻底隐藏");
// 如果当前已经是在收纳模式,立即隐藏
if (isFloatingBarFolded)
{
this.Visibility = Visibility.Hidden;
}
break;
case "thoroughhideoff":
Settings.Automation.ThoroughlyHideWhenFolded = false;
SaveSettingsToFile();
ShowNotification("已关闭:收起时彻底隐藏");
// 确保窗口可见
this.Visibility = Visibility.Visible;
break;
case "thoroughhidetoggle":
Settings.Automation.ThoroughlyHideWhenFolded = !Settings.Automation.ThoroughlyHideWhenFolded;
SaveSettingsToFile();
ShowNotification(Settings.Automation.ThoroughlyHideWhenFolded ? "已开启:收起时彻底隐藏" : "已关闭:收起时彻底隐藏");
if (isFloatingBarFolded)
{
this.Visibility = Settings.Automation.ThoroughlyHideWhenFolded ? Visibility.Hidden : Visibility.Visible;
}
break;
case "randone":
SymbolIconRandOne_MouseUp(null, null);
break;
case "rand":
SymbolIconRand_MouseUp(null, null);
break;
case "timer":
ImageCountdownTimer_MouseUp(null, null);
break;
case "whiteboard":
case "board":
ImageBlackboard_MouseUp(null, null);
break;
default:
LogHelper.WriteLogToFile($"未知的URI命令: {command}", LogHelper.LogType.Warning);
break;
+3
View File
@@ -496,6 +496,9 @@ namespace Ink_Canvas
[JsonProperty("autoSaveStrokesIntervalMinutes")]
public int AutoSaveStrokesIntervalMinutes { get; set; } = 5;
[JsonProperty("thoroughlyHideWhenFolded")]
public bool ThoroughlyHideWhenFolded { get; set; } = false;
[JsonProperty("floatingWindowInterceptor")]
public FloatingWindowInterceptorSettings FloatingWindowInterceptor { get; set; } = new FloatingWindowInterceptorSettings();
}
@@ -417,8 +417,8 @@
</Grid>
<Border Height="1" Background="#ebebeb" Margin="0,8,0,8"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0">
<Button x:Name="BtnManualBackup" Content="手动备份" Padding="12,6" Margin="0,0,12,0" Background="#2563eb" Foreground="White"/>
<Button x:Name="BtnRestoreBackup" Content="还原备份" Padding="12,6" Background="#2563eb" Foreground="White"/>
<Button x:Name="BtnManualBackup" Tag="manual_backup" Content="手动备份" Padding="12,6" Margin="0,0,12,0" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<Button x:Name="BtnRestoreBackup" Tag="restore_backup" Content="还原备份" Padding="12,6" Background="#2563eb" Foreground="White" Click="Button_Click"/>
</StackPanel>
</StackPanel>
</Border>
@@ -430,9 +430,9 @@
<TextBlock Text="管理.icstk文件的关联设置,双击.icstk文件可直接在Ink Canvas中打开"
TextWrapping="Wrap" Foreground="#9a9996" FontSize="11" Margin="0,0,0,12"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0">
<Button x:Name="BtnUnregisterFileAssociation" Content="取消文件关联" Padding="15,5" Margin="0,0,8,0" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<Button x:Name="BtnCheckFileAssociation" Content="检查关联状态" Padding="15,5" Margin="0,0,8,0" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<Button x:Name="BtnRegisterFileAssociation" Content="重新注册关联" Padding="15,5" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<Button x:Name="BtnUnregisterFileAssociation" Tag="unregister" Content="取消文件关联" Padding="15,5" Margin="0,0,8,0" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<Button x:Name="BtnCheckFileAssociation" Tag="check" Content="检查关联状态" Padding="15,5" Margin="0,0,8,0" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<Button x:Name="BtnRegisterFileAssociation" Tag="register" Content="重新注册关联" Padding="15,5" Background="#2563eb" Foreground="White" Click="Button_Click"/>
</StackPanel>
</StackPanel>
</Border>
@@ -448,7 +448,10 @@
<TextBlock Foreground="#2e3436" FontSize="14.5" Text="启用外部协议 (icc://)" HorizontalAlignment="Left"/>
<TextBlock Foreground="#9a9996" FontSize="11" Margin="0,3.5,0,0" Text="开启后可通过 icc://fold (收纳) 或 icc://unfold (展开) 等命令控制" HorizontalAlignment="Left"/>
</StackPanel>
<Border x:Name="ToggleSwitchIsEnableUriScheme" Style="{StaticResource ToggleSwitchStyle}" Background="#3584e4" Tag="IsEnableUriScheme" MouseLeftButtonDown="ToggleSwitch_Click">
<Border x:Name="ToggleSwitchIsEnableUriScheme" Style="{StaticResource ToggleSwitchStyle}" Background="#3584e4" Tag="IsEnableUriScheme" MouseLeftButtonDown="ToggleSwitch_Click"
Focusable="True" IsTabStop="True" KeyDown="ToggleSwitch_KeyDown"
AutomationProperties.Name="启用外部协议 (icc://)"
AutomationProperties.HelpText="开启后可通过 icc://fold (收纳) 或 icc://unfold (展开) 等命令控制">
<Border Width="19" Height="19" Background="White" CornerRadius="10" HorizontalAlignment="Right" VerticalAlignment="Center">
<Border.Effect>
<DropShadowEffect BlurRadius="4" Direction="-45" Color="Black" Opacity="0.3" ShadowDepth="0"/>
@@ -658,7 +661,7 @@
<Border Margin="0,25,0,0" BorderBrush="#e6e6e6" BorderThickness="1.25,1.25,1.25,4" CornerRadius="8">
<StackPanel Orientation="Vertical" Margin="18,18,18,18">
<TextBlock Text="Dlass设置管理" FontWeight="Bold" Foreground="#2e3436" FontSize="18" Margin="0,0,0,12"/>
<Button x:Name="BtnDlassSettingsManage" Content="Dlass设置管理" Padding="15,5" HorizontalAlignment="Left" Background="#2563eb" Foreground="White"/>
<Button x:Name="BtnDlassSettingsManage" Tag="dlass_settings" Content="Dlass设置管理" Padding="15,5" HorizontalAlignment="Left" Background="#2563eb" Foreground="White" Click="Button_Click"/>
</StackPanel>
</Border>
</StackPanel>
@@ -330,6 +330,18 @@ namespace Ink_Canvas.Windows.SettingsViews
}
}
/// <summary>
/// ToggleSwitch键盘事件处理
/// </summary>
private void ToggleSwitch_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Space || e.Key == System.Windows.Input.Key.Enter)
{
ToggleSwitch_Click(sender, new RoutedEventArgs());
e.Handled = true;
}
}
/// <summary>
/// Slider值变化事件处理
/// </summary>
@@ -466,31 +478,39 @@ namespace Ink_Canvas.Windows.SettingsViews
var button = sender as Button;
if (button == null) return;
string name = button.Name;
string action = button.Tag?.ToString();
if (string.IsNullOrEmpty(action)) action = button.Name;
// 这些按钮的功能可能需要调用 MainWindow 中的方法
// 暂时先留空,后续可以根据需要实现
switch (name)
switch (action)
{
case "manual_backup":
case "BtnManualBackup":
// TODO: 调用 MainWindow 的备份方法
break;
case "restore_backup":
case "BtnRestoreBackup":
// TODO: 调用 MainWindow 的还原方法
break;
case "unregister":
case "BtnUnregisterFileAssociation":
// TODO: 调用 MainWindow 的取消文件关联方法
break;
case "check":
case "BtnCheckFileAssociation":
// TODO: 调用 MainWindow 的检查文件关联方法
// TODO: 调用 MainWindow 的检查文件关联状态方法
break;
case "register":
case "BtnRegisterFileAssociation":
// TODO: 调用 MainWindow 的注册文件关联方法
break;
case "dlass_settings":
case "BtnDlassSettingsManage":
// TODO: 调用 MainWindow 的 Dlass 设置管理方法
break;