[Quicker] 剪贴板 - 源码归档
动作:剪贴板
WASD极速导航 | 收藏云同步 | 临时置顶 | 图片预览 | 一键收藏 | 零秒启动
更新时间:2025-12-28 18:27
原始文件:剪贴板.cs
核心代码
//css_ref PresentationCore
//css_ref PresentationFramework
//css_ref WindowsBase
//css_ref System.Xaml
//css_ref System.Xml
//css_ref System.Drawing
//css_ref System.Windows.Forms
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Threading;
using System.Xml.Serialization;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Media.Imaging;
using System;
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Windows.Media.Animation;
// --- 性能监控辅助类 ---
public static class Perf
{
private static Stopwatch _sw = new Stopwatch();
// 简单的耗时测量,超过阈值才记录
public static void Measure(string name, Action action, int thresholdMs = 15)
{
var sw = Stopwatch.StartNew();
try
{
action();
}
finally
{
sw.Stop();
long ms = sw.ElapsedMilliseconds;
if (ms >= thresholdMs)
{
ClipboardHistoryWindow.Log($"[Perf] {name} 耗时: {ms}ms");
}
}
}
public static T Measure<T>(string name, Func<T> func, int thresholdMs = 15)
{
var sw = Stopwatch.StartNew();
try
{
return func();
}
finally
{
sw.Stop();
long ms = sw.ElapsedMilliseconds;
if (ms >= thresholdMs)
{
ClipboardHistoryWindow.Log($"[Perf] {name} 耗时: {ms}ms");
}
}
}
// 新增:资源监控指标记录
public static void LogStats(string context)
{
try {
using (var proc = System.Diagnostics.Process.GetCurrentProcess()) {
double mem = proc.WorkingSet64 / 1024.0 / 1024.0;
double cpu = proc.TotalProcessorTime.TotalSeconds;
ClipboardHistoryWindow.Log($"[Metrics] {context} -> Memory: {mem:F1}MB, CPU Time: {cpu:F2}s");
}
} catch {}
}
}
public static void Exec(Quicker.Public.IStepContext context)
{
if (System.Threading.Thread.CurrentThread.GetApartmentState() != System.Threading.ApartmentState.STA)
{
System.Windows.Forms.MessageBox.Show("配置错误:\n请将【执行线程】修改为【后台线程 (STA 独立线程)】。");
return;
}
IntPtr targetWin = ClipboardHistoryWindow.GetForegroundWindow();
bool isSilentStart = false;
try {
object v = context.GetVarValue("silent");
if (v != null)
{
if (v is bool b) isSilentStart = b;
else if (v is string s) isSilentStart = s.Equals("true", StringComparison.OrdinalIgnoreCase);
}
} catch { }
// 1. 寻找已存在的窗口
Window existing = null;
// 优先尝试当前程序集的静态实例
if (ClipboardHistoryWindow.Instance != null)
{
try { if (ClipboardHistoryWindow.Instance.IsLoaded) existing = ClipboardHistoryWindow.Instance; } catch { }
}
// 如果没找到,通过全局窗口列表寻找(支持跨程序集唤起)
if (existing == null && System.Windows.Application.Current != null)
{
try {
System.Windows.Application.Current.Dispatcher.Invoke(() => {
existing = System.Windows.Application.Current.Windows.Cast<Window>()
.FirstOrDefault(w => {
try {
string t = w.Dispatcher.CheckAccess() ? w.Title : w.Dispatcher.Invoke(() => w.Title);
return t == "剪贴板历史" || t == "剪贴板1";
} catch { return false; }
});
});
} catch { }
}
if (existing != null)
{
ClipboardHistoryWindow.Log($"[Exec] 发现已有窗口,尝试切换显隐状态。");
existing.Dispatcher.Invoke(() => {
try {
var type = existing.GetType();
var updateMethod = type.GetMethod("UpdateTargetWindow");
if (updateMethod != null) updateMethod.Invoke(existing, new object[] { targetWin });
if (isSilentStart) return;
// 核心控制:切换显隐 (Toggle)
bool isVisible = existing.Visibility == Visibility.Visible;
ClipboardHistoryWindow.Log($"[Exec] 状态分析: Visibility={existing.Visibility}, IsVisible={existing.IsVisible}");
if (isVisible)
{
ClipboardHistoryWindow.Log($"[Exec] 窗口已显示,执行隐藏。");
var hideMethod = type.GetMethod("HideWindow");
if (hideMethod != null) hideMethod.Invoke(existing, null);
else existing.Visibility = Visibility.Hidden;
}
else
{
ClipboardHistoryWindow.Log($"[Exec] 窗口显示/唤醒。");
var showMethod = type.GetMethod("ShowAtMousePosition", new Type[] { typeof(bool) });
if (showMethod == null) showMethod = type.GetMethod("ShowAtMousePosition", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (showMethod != null)
{
if (showMethod.GetParameters().Length == 1)
showMethod.Invoke(existing, new object[] { true });
else
showMethod.Invoke(existing, null);
}
else {
existing.Visibility = Visibility.Visible;
existing.Show();
existing.Activate();
// If ShowAtMousePosition is not found, and we are showing the window,
// we should also try to refresh the collection view if it's a ClipboardHistoryWindow.
if (existing is ClipboardHistoryWindow chw)
{
chw.RefreshCollectionView(); // Call the new method directly if possible
}
}
}
} catch (Exception ex) {
ClipboardHistoryWindow.Log($"[Exec] 唤醒过程出错: {ex.Message}");
}
});
return;
}
try
{
ClipboardHistoryWindow.Log($"[Exec] 创建全新窗口. Silent={isSilentStart}");
var win = new ClipboardHistoryWindow(context, targetWin, isSilentStart);
if (!isSilentStart)
{
win.Show();
win.Activate();
}
// 阻塞在消息循环,直到窗口关闭
System.Windows.Threading.Dispatcher.Run();
}
catch (Exception ex)
{
ClipboardHistoryWindow.Log($"[Exec] 崩溃退出: {ex.Message}\n{ex.StackTrace}");
}
}
public enum ClipType { Text = 0, File = 1, Image = 2 }
public enum ContentCategory
{
General,
URL,
Email,
Phone,
Code,
JsonXml,
Color,
FilePath,
DateTime,
IPAddress,
File,
Image
}
public class ClipItem : INotifyPropertyChanged
{
private bool _isFavorite;
private bool _isPinned;
private string _text;
private string _dataValue;
private ClipType _type;
private DateTime _timestamp;
private int _index;
private string _sourcePath;
private string _targetPath;
private BitmapImage _cachedImage;
private ImageSource _sourceIcon;
private ImageSource _targetIcon;
public string ImageHash { get; set; } // 新增:保存图片哈希
[XmlIgnore]
public string IndexDisplay => _index.ToString("00");
[XmlIgnore]
public int Index
{
get { return _index; }
set { _index = value; OnPropertyChanged("Index"); OnPropertyChanged("IndexDisplay"); }
}
[XmlIgnore] public IntPtr SourceWindowHandle { get; set; }
[XmlIgnore] public IntPtr TargetWindowHandle { get; set; }
public string Text
{
get { return _text; }
set {
_text = value;
OnPropertyChanged("Text");
UpdateCategory();
}
}
private ContentCategory _category = ContentCategory.General;
public ContentCategory Category
{
get { return _category; }
set { _category = value; OnPropertyChanged("Category"); OnPropertyChanged("CategoryLabel"); OnPropertyChanged("CategoryIcon"); OnPropertyChanged("CategoryColor"); }
}
[XmlIgnore]
public string CategoryLabel
{
get {
switch (Category) {
case ContentCategory.URL: return "链接";
case ContentCategory.Email: return "邮箱";
case ContentCategory.Phone: return "电话";
case ContentCategory.Code: return "代码";
case ContentCategory.JsonXml: return "结构化";
case ContentCategory.Color: return "颜色";
case ContentCategory.FilePath: return "路径";
case ContentCategory.DateTime: return "时间";
case ContentCategory.IPAddress: return "IP";
case ContentCategory.File: return "文件";
case ContentCategory.Image: return "图片";
default: return "";
}
}
}
[XmlIgnore]
public string CategoryIcon
{
get {
switch (Category) {
case ContentCategory.URL: return "";
case ContentCategory.Email: return "";
case ContentCategory.Phone: return "";
case ContentCategory.Code: return "</>";
case ContentCategory.JsonXml: return "{ }";
case ContentCategory.Color: return "";
case ContentCategory.FilePath: return "";
case ContentCategory.DateTime: return "";
case ContentCategory.IPAddress: return "";
case ContentCategory.File: return "";
case ContentCategory.Image: return ""; // 移除图片图标
default: return ""; // 移除文字默认图标
}
}
}
[XmlIgnore]
public string CategoryColor
{
get {
switch (Category) {
case ContentCategory.URL: return "#10b981";
case ContentCategory.Email: return "#e67e22";
case ContentCategory.Phone: return "#2ecc71";
case ContentCategory.Code: return "#3b82f6";
case ContentCategory.JsonXml: return "#8b5cf6";
case ContentCategory.Color: return "#ec4899";
case ContentCategory.FilePath: return "#f1c40f";
case ContentCategory.DateTime: return "#7f8c8d";
case ContentCategory.IPAddress: return "#34495e";
case ContentCategory.File: return "#95a5a6";
case ContentCategory.Image: return "#16a085";
default: return "#9ca3af";
}
}
}
[XmlIgnore]
public Visibility CategoryTagVisibility => Visibility.Collapsed; // 彻底移除标签
[XmlIgnore]
public Visibility CategoryVisibility => (Category != ContentCategory.General && Category != ContentCategory.Image) ? Visibility.Visible : Visibility.Collapsed;
// ToolTip 智能显示:仅当文本被截断时才显示
[XmlIgnore]
public string ToolTipText
{
get
{
if (Type == ClipType.Image) return null; // 图片单独处理
if (string.IsNullOrEmpty(Text)) return null;
// 文本长度小于 16 字符且无换行,认为未被截断
if (Text.Length <= 16 && !Text.Contains("\n")) return null;
return Text;
}
}
// 预编译正则表达式以获得最佳性能
private static readonly Regex _regexUrl = new Regex(@"^(https?|ftp)://[^\s/$.?#].[^\s]*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex _regexEmail = new Regex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$", RegexOptions.Compiled);
private static readonly Regex _regexHexColor = new Regex(@"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$", RegexOptions.Compiled);
private static readonly Regex _regexRgbColor = new Regex(@"^(rgb|hsl)a?\(.*\)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex _regexWinPath = new Regex(@"^[a-zA-Z]:\\[\\\S|*\s]*$", RegexOptions.Compiled);
private static readonly Regex _regexPhone = new Regex(@"^(\+86)?\d{11}$|^(\d{3,4}-)?\d{7,8}$", RegexOptions.Compiled);
private static readonly Regex _regexIP = new Regex(@"^(\d{1,3}\.){3}\d{1,3}$", RegexOptions.Compiled);
public void UpdateCategory()
{
if (Type == ClipType.File) { Category = ContentCategory.File; return; }
if (Type == ClipType.Image) { Category = ContentCategory.Image; return; }
if (Type != ClipType.Text || string.IsNullOrWhiteSpace(Text))
{
Category = ContentCategory.General;
return;
}
string t = Text.Trim();
if (_regexUrl.IsMatch(t)) { Category = ContentCategory.URL; return; }
if (_regexEmail.IsMatch(t)) { Category = ContentCategory.Email; return; }
if (_regexHexColor.IsMatch(t) || _regexRgbColor.IsMatch(t)) { Category = ContentCategory.Color; return; }
if (_regexWinPath.IsMatch(t)) { Category = ContentCategory.FilePath; return; }
if (_regexPhone.IsMatch(t)) { Category = ContentCategory.Phone; return; }
if (_regexIP.IsMatch(t)) { Category = ContentCategory.IPAddress; return; }
// JSON/XML
if ((t.StartsWith("{") && t.EndsWith("}")) || (t.StartsWith("[") && t.EndsWith("]"))) { Category = ContentCategory.JsonXml; return; }
if (t.StartsWith("<") && t.EndsWith(">")) { Category = ContentCategory.JsonXml; return; }
// Code (Heuristic)
if (t.Contains(";") && (t.Contains("{") || t.Contains("public") || t.Contains("var ") || t.Contains("function") || t.Contains("import "))) { Category = ContentCategory.Code; return; }
Category = ContentCategory.General;
}
public string DataValue
{
get { return _dataValue; }
set {
_dataValue = value;
_cachedImage = null;
OnPropertyChanged("Thumbnail");
OnPropertyChanged("PreviewImagePath");
}
}
public ClipType Type
{
get { return _type; }
set {
_type = value;
OnPropertyChanged("TextVisibility");
OnPropertyChanged("ImageVisibility");
OnPropertyChanged("Thumbnail");
OnPropertyChanged("PreviewImagePath");
}
}
public bool IsFavorite
{
get { return _isFavorite; }
set { _isFavorite = value; OnPropertyChanged("IsFavorite"); OnPropertyChanged("IconVisibility"); }
}
public bool IsPinned
{
get { return _isPinned; }
set { _isPinned = value; OnPropertyChanged("IsPinned"); }
}
public DateTime Timestamp
{
get { return _timestamp; }
set { _timestamp = value; OnPropertyChanged("DisplayTime"); OnPropertyChanged("FriendlyTime"); }
}
public string SourcePath
{
get { return _sourcePath; }
set { _sourcePath = value; _sourceIcon = null; OnPropertyChanged("SourcePath"); OnPropertyChanged("SourceAppIcon"); OnPropertyChanged("SourceAppName"); OnPropertyChanged("HasSource"); }
}
public string TargetPath
{
get { return _targetPath; }
set { _targetPath = value; _targetIcon = null; OnPropertyChanged("TargetPath"); OnPropertyChanged("TargetAppIcon"); OnPropertyChanged("TargetAppName"); OnPropertyChanged("HasTarget"); }
}
[XmlIgnore] public string DisplayTime { get { return Timestamp.ToString("HH:mm"); } }
[XmlIgnore]
public string FriendlyTime
{
get
{
var span = DateTime.Now - Timestamp;
if (span.TotalMinutes < 1) return "刚刚";
if (span.TotalMinutes < 60) return string.Format("{0}分钟前", (int)span.TotalMinutes);
if (span.TotalHours < 24) return string.Format("{0}小时前", (int)span.TotalHours);
if (span.TotalDays < 2) return "昨天";
return Timestamp.ToString("MM-dd");
}
}
[XmlIgnore] public string IconVisibility { get { return IsFavorite ? "Visible" : "Collapsed"; } }
[XmlIgnore] public Visibility TextVisibility { get { return string.IsNullOrWhiteSpace(Text) ? Visibility.Collapsed : Visibility.Visible; } }
[XmlIgnore] public Visibility ImageVisibility { get { return Thumbnail != null ? Visibility.Visible : Visibility.Collapsed; } }
[XmlIgnore] public Visibility HasSource { get { return string.IsNullOrEmpty(SourcePath) ? Visibility.Collapsed : Visibility.Visible; } }
[XmlIgnore] public Visibility HasTarget { get { return string.IsNullOrEmpty(TargetPath) ? Visibility.Collapsed : Visibility.Visible; } }
[XmlIgnore] public string SourceAppName { get { return string.IsNullOrEmpty(SourcePath) ? "" : System.IO.Path.GetFileNameWithoutExtension(SourcePath); } }
[XmlIgnore] public string TargetAppName { get { return string.IsNullOrEmpty(TargetPath) ? "" : System.IO.Path.GetFileNameWithoutExtension(TargetPath); } }
[XmlIgnore]
public ImageSource SourceAppIcon
{
get
{
if (_sourceIcon == null && !string.IsNullOrEmpty(SourcePath))
_sourceIcon = ClipboardHistoryWindow.GetIconFromPath(SourcePath);
return _sourceIcon;
}
}
[XmlIgnore]
public ImageSource TargetAppIcon
{
get
{
if (_targetIcon == null && !string.IsNullOrEmpty(TargetPath))
_targetIcon = ClipboardHistoryWindow.GetIconFromPath(TargetPath);
return _targetIcon;
}
}
// 预览大图(原始路径)
[XmlIgnore]
public string PreviewImagePath
{
get
{
if (Type == ClipType.Image && !string.IsNullOrEmpty(DataValue)) return DataValue;
if (Type == ClipType.File && !string.IsNullOrEmpty(DataValue))
{
var paths = DataValue.Split('|');
if (paths.Length > 0 && IsImageFile(paths[0])) return paths[0];
}
return null;
}
}
// 列表缩略图(压缩)
[XmlIgnore]
public ImageSource Thumbnail
{
get
{
if (_cachedImage != null) return _cachedImage;
if (string.IsNullOrEmpty(DataValue)) return null;
string imagePath = PreviewImagePath;
if (!string.IsNullOrEmpty(imagePath) && File.Exists(imagePath))
{
try
{
var bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(imagePath);
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.DecodePixelHeight = 100;
bi.EndInit();
bi.Freeze();
_cachedImage = bi;
return _cachedImage;
}
catch { }
}
return null;
}
}
private bool IsImageFile(string path)
{
try {
string ext = System.IO.Path.GetExtension(path).ToLower();
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".gif" || ext == ".webp";
} catch { return false; }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); }
}
public class ClipboardHistoryWindow : Window
{
// 依赖属性:用于绑定 DragHandle 的显示状态
public bool IsFavMode
{
get { return (bool)GetValue(IsFavModeProperty); }
set { SetValue(IsFavModeProperty, value); }
}
public static ClipboardHistoryWindow Instance;
public static readonly DependencyProperty IsFavModeProperty =
DependencyProperty.Register("IsFavMode", typeof(bool), typeof(ClipboardHistoryWindow), new PropertyMetadata(false));
private Quicker.Public.IStepContext _context;
private ObservableCollection<ClipItem> _items = new ObservableCollection<ClipItem>();
private ICollectionView _itemsView;
private TextBlock _tabAll;
private TextBlock _tabFav;
private TextBlock _syncIcon; // 同步状态图标
private ListBox _listBox;
private TextBox _searchBox;
private string _dataFile;
private string _appDataPath;
private string _configFile;
private string _imageCacheDir;
private DispatcherTimer _monitorTimer;
private string _lastClipboardSignature;
private bool _showFavoritesOnly = false;
private string _searchText = "";
private bool _mouseActivateAtFirstItem = false;
private ContentCategory _selectedCategory = ContentCategory.General; // General here means "All"
private bool _isDarkTheme = false;
private IntPtr _targetWindow;
private bool _isModalOpen = false;
private bool _forceClose = false;
private bool _silentStart = false;
private System.Windows.Controls.Primitives.ToggleButton _pinWindowBtn;
public bool IsPinnedMode { get { return _pinWindowBtn != null && _pinWindowBtn.IsChecked == true; } }
private int _tickCounter = 0;
private uint _lastClipboardSequenceNumber = 0;
public DateTime _lastShowTime = DateTime.MinValue; // 用于防止呼出时失焦闪退
private static readonly object _saveLock = new object(); // 用于防止后台保存冲突
private DispatcherTimer _searchTimer; // 搜索防抖定时器
// 快速滚动选择模式
private bool _isQuickSelectMode = false;
private bool _hasScrolledInQuickMode = false; // 是否在快速模式下滚动过
private DateTime _lastQuickModeTime = DateTime.MinValue; // 防止快速重复触发
private bool _quickModeJustExited = false; // 刚从快速模式退出(用于防止窗口被隐藏)
// 快捷键配置(默认 Ctrl+Alt+V,避免与系统输入法切换 Ctrl+Shift 冲突)
private bool _hotkeyCtrl = true; // 是否需要Ctrl
private bool _hotkeyShift = false; // 是否需要Shift
private bool _hotkeyAlt = true; // 是否需要Alt
private int _hotkeyKey = 0x56; // 主键,默认V (0x56)
private string _settingsFile;
// 公开静态日志方法,供辅助类调用
public static void Log(string msg)
{
try {
// 记录日志到 AppData,避免 GetCurrentDirectory 的不确定性
string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Quicker_ClipFlow");
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
string path = Path.Combine(dir, "detailed_debug.log");
string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
string logEntry = $"[{time}] [Thread:{System.Threading.Thread.CurrentThread.ManagedThreadId}] {msg}\r\n";
// 日志大小控制:超过 5MB 直接删除重建
try {
var info = new FileInfo(path);
if (info.Exists && info.Length > 5 * 1024 * 1024) {
File.Delete(path);
}
} catch {}
File.AppendAllText(path, logEntry);
} catch {}
}
private void DebugLog(string msg)
{
Log(msg);
}
public void ShowDebugLogs()
{
try {
string path = Path.Combine(_appDataPath, "perf_debug.log");
if (File.Exists(path))
Process.Start("notepad.exe", path);
} catch {}
}
private IntPtr _keyboardHookId = IntPtr.Zero;
private IntPtr _mouseHookId = IntPtr.Zero;
private LowLevelKeyboardProc _keyboardProc;
private LowLevelMouseProc _mouseProc;
// 全局钩子相关
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
private const int WH_KEYBOARD_LL = 13;
private const int WH_MOUSE_LL = 14;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private const int WM_MOUSEWHEEL = 0x020A;
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
private const int VK_CONTROL = 0x11;
private const int VK_SHIFT = 0x10;
private const int VK_ALT = 0x12;
private const int VK_C = 0x43;
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern uint GetClipboardSequenceNumber();
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
private const int KEYEVENTF_KEYUP = 0x0002;
[DllImport("user32.dll", SetLastError=true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
internal static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool IsWindow(IntPtr hWnd);
private const int SW_RESTORE = 9;
[StructLayout(LayoutKind.Sequential)]
public struct POINT { public int X; public int Y; }
public ClipboardHistoryWindow(Quicker.Public.IStepContext context, IntPtr targetWindow, bool silentStart = false)
{
_context = context;
_targetWindow = targetWindow;
_silentStart = silentStart;
_appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Quicker_ClipFlow");
if (!Directory.Exists(_appDataPath)) Directory.CreateDirectory(_appDataPath);
_dataFile = Path.Combine(_appDataPath, "History_v7.xml");
_configFile = Path.Combine(_appDataPath, "Config.txt");
_settingsFile = Path.Combine(_appDataPath, "Settings.txt");
_imageCacheDir = Path.Combine(_appDataPath, "Images");
if (!Directory.Exists(_imageCacheDir)) Directory.CreateDirectory(_imageCacheDir);
Instance = this;
LoadSettings();
DetectSystemTheme();
this.Title = "剪贴板历史";
this.Width = 360;
this.Height = 560;
this.WindowStyle = WindowStyle.None;
this.ResizeMode = ResizeMode.CanResize;
this.MinWidth = 300;
this.MinHeight = 400;
this.AllowsTransparency = true;
this.Background = Brushes.Transparent;
// 如果是静默启动,初始完全透明并隐藏
// 如果是正常启动,初始可见度设为1,防止 Loaded 时闪烁
this.Opacity = silentStart ? 0 : 1;
this.Visibility = silentStart ? Visibility.Hidden : Visibility.Visible;
this.ShowInTaskbar = !silentStart;
this.WindowStartupLocation = WindowStartupLocation.Manual;
LoadThemeResources();
Perf.Measure("InitializeUI", () => InitializeUI(), 1);
Perf.LogStats("StartupComplete"); // 记录启动后的资源占用
// 启用缩放支持
var chrome = new System.Windows.Shell.WindowChrome {
CaptionHeight = 0,
ResizeBorderThickness = new Thickness(5),
GlassFrameThickness = new Thickness(0)
};
System.Windows.Shell.WindowChrome.SetWindowChrome(this, chrome);
LoadData();
CleanupImageCache(false);
// LoadWindowPosition 移至 Loaded 中统一处理,避免二次计算坐标
this.Loaded += (s, e) => {
this.Opacity = 1;
if (!_silentStart)
{
Log($"[Loaded] 窗口加载完成,执行初次显示逻辑。");
_lastShowTime = DateTime.Now;
this.ShowInTaskbar = true;
if (_mouseActivateAtFirstItem) ShowAtMousePosition(true);
else UpdatePositionToMouse();
ResetFocus();
this.Visibility = Visibility.Visible;
this.Activate();
}
else
{
Log($"[Loaded] 静默启动完成,窗口维持隐藏状态。");
}
};
CheckClipboardAndAdd(true);
_monitorTimer = new DispatcherTimer();
_monitorTimer.Interval = TimeSpan.FromMilliseconds(50);
_monitorTimer.Tick += new EventHandler(OnTimerTick);
_monitorTimer.Start();
this.Deactivated += new EventHandler(OnDeactivated);
this.Closing += new System.ComponentModel.CancelEventHandler(OnClosing);
this.PreviewKeyDown += new KeyEventHandler(OnWindowKeyDown);
this.PreviewKeyUp += new KeyEventHandler(OnWindowKeyUp);
this.PreviewMouseWheel += new MouseWheelEventHandler(OnWindowMouseWheel);
this.MouseLeftButtonDown += (o, args) => {
// 只在空白区域拖动,避免拦截按钮点击
if (args.OriginalSource is Border || args.OriginalSource is Grid || args.OriginalSource is Window)
{
if (args.ButtonState == MouseButtonState.Pressed)
this.DragMove();
}
};
// 初始化全局钩子
InstallGlobalHooks();
}
private void InstallGlobalHooks()
{
_keyboardProc = KeyboardHookCallback;
_mouseProc = MouseHookCallback;
using (var curProcess = Process.GetCurrentProcess())
using (var curModule = curProcess.MainModule)
{
_keyboardHookId = SetWindowsHookEx(WH_KEYBOARD_LL, _keyboardProc, GetModuleHandle(curModule.ModuleName), 0);
_mouseHookId = SetWindowsHookEx(WH_MOUSE_LL, _mouseProc, GetModuleHandle(curModule.ModuleName), 0);
}
}
private void UninstallGlobalHooks()
{
if (_keyboardHookId != IntPtr.Zero)
{
UnhookWindowsHookEx(_keyboardHookId);
_keyboardHookId = IntPtr.Zero;
}
if (_mouseHookId != IntPtr.Zero)
{
UnhookWindowsHookEx(_mouseHookId);
_mouseHookId = IntPtr.Zero;
}
}
private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
int vkCode = Marshal.ReadInt32(lParam);
// 检测配置的快捷键
bool ctrlPressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
bool shiftPressed = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
bool altPressed = (GetAsyncKeyState(VK_ALT) & 0x8000) != 0;
bool hotkeyMatch = vkCode == _hotkeyKey &&
ctrlPressed == _hotkeyCtrl &&
shiftPressed == _hotkeyShift &&
altPressed == _hotkeyAlt;
if (hotkeyMatch)
{
if ((int)wParam == WM_KEYDOWN)
{
DebugLog($"[KeyDown C] isQuick={_isQuickSelectMode}, hasScrolled={_hasScrolledInQuickMode}");
// 已经在快速模式中,忽略重复的KeyDown
if (_isQuickSelectMode)
{
DebugLog("[KeyDown C] 忽略:已在快速模式");
return (IntPtr)1;
}
// 防止快速重复触发(300ms内不重复触发)
var elapsed = (DateTime.Now - _lastQuickModeTime).TotalMilliseconds;
if (elapsed < 300)
{
DebugLog($"[KeyDown C] 忽略:间隔太短 {elapsed:F0}ms");
return (IntPtr)1;
}
// 如果窗口已经显示,则隐藏它(切换显示)
bool isVisible = false;
Dispatcher.Invoke(() => { isVisible = this.IsVisible; });
if (isVisible)
{
DebugLog("[KeyDown C] 窗口已显示,隐藏窗口");
_lastQuickModeTime = DateTime.Now;
Dispatcher.BeginInvoke(new Action(() => { HideWindow(); }));
return (IntPtr)1;
}
// 同步设置标记,避免异步竞争
_isQuickSelectMode = true;
_hasScrolledInQuickMode = false;
_lastQuickModeTime = DateTime.Now;
var targetWin = GetForegroundWindow();
Perf.Measure("Hook_KeyDown_Show", () => {
DebugLog("[KeyDown C] 进入快速模式");
Dispatcher.BeginInvoke(new Action(() => {
_targetWindow = targetWin;
ShowAtMousePosition(true);
}));
});
return (IntPtr)1; // 阻止默认行为
}
else if ((int)wParam == WM_KEYUP)
{
DebugLog($"[KeyUp C] isQuick={_isQuickSelectMode}, hasScrolled={_hasScrolledInQuickMode}");
if (!_isQuickSelectMode)
{
DebugLog("[KeyUp C] 忽略:不在快速模式");
return CallNextHookEx(_keyboardHookId, nCode, wParam, lParam);
}
// 同步读取标记
bool shouldPaste = _hasScrolledInQuickMode;
_isQuickSelectMode = false;
_hasScrolledInQuickMode = false;
DebugLog($"[KeyUp C] 退出快速模式, shouldPaste={shouldPaste}");
if (shouldPaste)
{
Dispatcher.BeginInvoke(new Action(() => {
var item = _listBox.SelectedItem as ClipItem;
DebugLog($"[KeyUp C] 执行粘贴: {(item != null ? item.Text?.Substring(0, Math.Min(20, item.Text?.Length ?? 0)) : "null")}");
if (item != null)
CopyAndClose(item);
else
HideWindow();
}));
}
else
{
// 没有滚动,标记刚退出快速模式,防止窗口被隐藏
_quickModeJustExited = true;
Dispatcher.BeginInvoke(new Action(() => {
_quickModeJustExited = false;
// 确保窗口获得焦点
this.Activate();
ResetFocus();
}));
}
return (IntPtr)1;
}
}
// 如果在快速选择模式下松开了任意修饰键
bool isModifierKey = vkCode == VK_CONTROL || vkCode == VK_SHIFT || vkCode == VK_ALT;
if (_isQuickSelectMode && (int)wParam == WM_KEYUP && isModifierKey)
{
string keyName = vkCode == VK_CONTROL ? "Ctrl" : (vkCode == VK_SHIFT ? "Shift" : "Alt");
DebugLog($"[KeyUp {keyName}] isQuick={_isQuickSelectMode}, hasScrolled={_hasScrolledInQuickMode}");
bool shouldPaste = _hasScrolledInQuickMode;
_isQuickSelectMode = false;
_hasScrolledInQuickMode = false;
DebugLog($"[KeyUp {keyName}] 退出快速模式, shouldPaste={shouldPaste}");
if (shouldPaste)
{
Dispatcher.BeginInvoke(new Action(() => {
var item = _listBox.SelectedItem as ClipItem;
DebugLog($"[KeyUp {keyName}] 执行粘贴");
if (item != null)
CopyAndClose(item);
else
HideWindow();
}));
}
else
{
// 没有滚动,标记刚退出快速模式,防止窗口被隐藏
_quickModeJustExited = true;
Dispatcher.BeginInvoke(new Action(() => {
_quickModeJustExited = false;
this.Activate();
ResetFocus();
}));
}
}
}
return CallNextHookEx(_keyboardHookId, nCode, wParam, lParam);
}
private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && (int)wParam == WM_MOUSEWHEEL)
{
// 只有在快速选择模式下才处理滚轮
if (!_isQuickSelectMode)
{
return CallNextHookEx(_mouseHookId, nCode, wParam, lParam);
}
// 再次检查窗口是否可见
bool isVisible = false;
try { isVisible = Dispatcher.Invoke(() => this.IsVisible); } catch { }
if (!isVisible)
{
_isQuickSelectMode = false; // 状态修正
return CallNextHookEx(_mouseHookId, nCode, wParam, lParam);
}
DebugLog($"[MouseWheel] 滚动,设置 hasScrolled=true");
// 获取滚轮方向
int delta = (short)((Marshal.ReadInt32(lParam, 8) >> 16) & 0xFFFF);
// 同步设置标记
_hasScrolledInQuickMode = true;
Dispatcher.BeginInvoke(new Action(() => {
if (_listBox != null)
{
int currentIndex = _listBox.SelectedIndex;
int newIndex = currentIndex;
if (delta < 0) // 向下滚
newIndex = Math.Min(currentIndex + 1, _listBox.Items.Count - 1);
else // 向上滚
newIndex = Math.Max(currentIndex - 1, 0);
if (newIndex != currentIndex && newIndex >= 0)
{
_listBox.SelectedIndex = newIndex;
_listBox.ScrollIntoView(_listBox.SelectedItem);
}
}
}));
return (IntPtr)1; // 阻止滚轮事件传递
}
return CallNextHookEx(_mouseHookId, nCode, wParam, lParam);
}
public void ShowAtMousePosition(bool isMouseActivate)
{
Log($"[ShowAtMousePosition] 激活窗口. MouseActivate={isMouseActivate}");
// --- 重置状态逻辑 ---
bool needRefresh = false;
// 1. 重置搜索
if (!string.IsNullOrEmpty(_searchBox.Text))
{
_searchBox.Text = "";
_searchText = "";
needRefresh = true;
}
// 2. 重置标签页到“全部”
if (IsFavMode || _selectedCategory != ContentCategory.General)
{
_selectedCategory = ContentCategory.General;
SwitchTab(false); // SwitchTab 内部会 Refresh
needRefresh = false; // 避免重复刷新
}
if (needRefresh && _itemsView != null) _itemsView.Refresh();
// ------------------
if (needRefresh && _itemsView != null) _itemsView.Refresh();
// ------------------
if (!_mouseActivateAtFirstItem)
{
// 如果未启用鼠标跟随,则恢复原来的记忆位置逻辑
UpdatePositionToMouse(); // 其实是 LoadPosition
this.ShowInTaskbar = true;
this.Show();
this.Activate();
if (_listBox.Items.Count > 0 && _listBox.SelectedIndex < 0) _listBox.SelectedIndex = 0;
return;
}
POINT pt;
GetCursorPos(out pt);
// DPI 适配:将物理像素转换为逻辑像素
double scaleX = 1.0, scaleY = 1.0;
try {
var source = PresentationSource.FromVisual(this);
if (source != null && source.CompositionTarget != null) {
scaleX = source.CompositionTarget.TransformToDevice.M11;
scaleY = source.CompositionTarget.TransformToDevice.M22;
}
} catch {}
double logicalMouseX = pt.X / scaleX;
double logicalMouseY = pt.Y / scaleY;
// 窗口显示在鼠标位置,稍微偏移避免遮挡
this.Left = logicalMouseX + 10;
this.Top = logicalMouseY - 100;
Log($"[ShowAtMousePosition] 最终定位: Left={this.Left}, Top={this.Top}");
// 确保不超出工作区(排除任务栏)
var workArea = SystemParameters.WorkArea;
if (this.Left + this.Width > workArea.Right)
this.Left = logicalMouseX - this.Width - 10;
if (this.Left < workArea.Left)
this.Left = workArea.Left;
if (this.Top < workArea.Top)
this.Top = workArea.Top;
if (this.Top + this.Height > workArea.Bottom)
this.Top = workArea.Bottom - this.Height;
bool wasHidden = this.Visibility != Visibility.Visible;
this.Visibility = Visibility.Visible;
this.Show();
if (wasHidden)
{
RefreshCollectionView();
}
if (isMouseActivate)
{
this.Activate();
this.Focus();
ResetFocus();
}
if (_listBox.Items.Count > 0 && _listBox.SelectedIndex < 0)
_listBox.SelectedIndex = 0;
// 强制重绘一下,防止位置更新延迟
// this.InvalidateVisual();
}
public void UpdateTargetWindow(IntPtr target) { _targetWindow = target; }
public void ResetFocus()
{
if (_listBox != null) {
_listBox.Focus();
if (_listBox.Items.Count > 0) {
_listBox.SelectedIndex = 0;
var item = _listBox.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem;
if(item != null) item.Focus();
}
}
}
private ToolTip _activeToolTip; // 追踪当前打开的 ToolTip
private DispatcherTimer _tooltipTimer;
private void ShowSelectedItemToolTip()
{
// 先关闭之前的 ToolTip
CloseActiveToolTip();
if (_listBox.SelectedIndex < 0) return;
// 检查是否需要显示 ToolTip
var item = _listBox.SelectedItem as ClipItem;
if (item == null) return;
// 文本类型:检查是否需要显示(未截断则不显示)
// 图片类型:始终显示预览
bool needsToolTip = (item.Type == ClipType.Image) || (item.ToolTipText != null);
if (!needsToolTip) return;
// 获取当前选中的 ListBoxItem
var container = _listBox.ItemContainerGenerator.ContainerFromIndex(_listBox.SelectedIndex) as ListBoxItem;
if (container == null) return;
// 找到内容区域的 StackPanel
var contentPanel = FindVisualChild<StackPanel>(container);
if (contentPanel == null) return;
// 创建 ToolTip 内容
object tooltipContent;
if (item.Type == ClipType.Image && !string.IsNullOrEmpty(item.PreviewImagePath))
{
// 图片预览
var img = new Image
{
Source = new BitmapImage(new Uri(item.PreviewImagePath)),
MaxWidth = 500,
MaxHeight = 400,
Stretch = Stretch.Uniform
};
tooltipContent = img;
}
else
{
// 文本预览
tooltipContent = item.ToolTipText;
}
// 创建并显示 ToolTip
var tooltip = new ToolTip
{
Content = tooltipContent,
Placement = System.Windows.Controls.Primitives.PlacementMode.Right,
PlacementTarget = contentPanel,
IsOpen = true,
MaxWidth = 500
};
_activeToolTip = tooltip;
// 3秒后自动关闭
_tooltipTimer?.Stop();
_tooltipTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
_tooltipTimer.Tick += (s, e) => { CloseActiveToolTip(); };
_tooltipTimer.Start();
}
private void CloseActiveToolTip()
{
if (_activeToolTip != null)
{
_activeToolTip.IsOpen = false;
_activeToolTip = null;
}
_tooltipTimer?.Stop();
}
private static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T result) return result;
var found = FindVisualChild<T>(child);
if (found != null) return found;
}
return null;
}
public void HideWindow()
{
SaveWindowPosition();
this.Hide();
this.ShowInTaskbar = false;
}
public void Quit()
{
Log("[Quit] 用户请求退出");
Instance = null;
_forceClose = true;
CleanupResources();
this.Close();
// 不再主动关闭 Dispatcher,交给 Quicker 管理线程生命周期
}
public void UpdatePositionToMouse()
{
this.ShowInTaskbar = true;
bool loadedFromConfig = false;
try {
if (File.Exists(_configFile))
{
var parts = File.ReadAllText(_configFile).Split(',');
if (parts.Length == 2)
{
double l = double.Parse(parts[0]);
double t = double.Parse(parts[1]);
// 屏幕越界检查:必须确保窗口的大部分都在可见区域内 (使用窗口宽高进行判定)
var screenW = SystemParameters.VirtualScreenWidth;
var screenH = SystemParameters.VirtualScreenHeight;
var screenL = SystemParameters.VirtualScreenLeft;
var screenT = SystemParameters.VirtualScreenTop;
if (l + 100 > screenL + screenW || l < screenL ||
t + 100 > screenT + screenH || t < screenT)
{
Log($"[UpdatePosition] 发现坐标越界 ({l}, {t}),虚拟屏幕边界: L={screenL}, T={screenT}, W={screenW}, H={screenH}");
}
else
{
this.Left = l;
this.Top = t;
Log($"[UpdatePosition] 成功恢复记忆位置: Left={l}, Top={t}");
loadedFromConfig = true;
}
}
}
} catch (Exception ex) {
Log($"[UpdatePosition] 配置文件读取异常: {ex.Message}");
}
if (!loadedFromConfig)
{
Log("[UpdatePosition] 设置居中显示 (Fallback)");
this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
}
private void SaveWindowPosition()
{
try {
File.WriteAllText(_configFile, $"{this.Left},{this.Top}");
} catch { }
}
private void LoadWindowPosition()
{
UpdatePositionToMouse();
}
private void CleanupImageCache(bool aggressive)
{
try {
if (_items == null) return;
var dir = new DirectoryInfo(_imageCacheDir);
var activeFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in _items)
{
if (item.Type == ClipType.Image && !string.IsNullOrEmpty(item.DataValue))
try { activeFiles.Add(Path.GetFullPath(item.DataValue)); } catch {}
}
foreach (var file in dir.GetFiles()) {
if (activeFiles.Contains(file.FullName)) continue;
if (aggressive || DateTime.Now - file.CreationTime > TimeSpan.FromDays(7))
try { file.Delete(); } catch { }
}
} catch { }
}
private string GetBitmapHash(BitmapSource source)
{
using (var ms = new MemoryStream())
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));
encoder.Save(ms);
ms.Position = 0;
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(ms);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
}
public static ImageSource GetIconFromPath(string path) {
try {
if (!File.Exists(path)) return null;
using (var sysicon = System.Drawing.Icon.ExtractAssociatedIcon(path)) {
return System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(
sysicon.Handle, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}
} catch { return null; }
}
private string GetProcessPath(IntPtr hwnd) {
try {
if (hwnd == IntPtr.Zero) return null;
uint pid;
GetWindowThreadProcessId(hwnd, out pid);
if (pid == 0) return null;
var p = Process.GetProcessById((int)pid);
if (p != null) return p.MainModule.FileName;
} catch { }
return null;
}
private void ActivateApp(string path, IntPtr hwnd)
{
bool activated = false;
if (hwnd != IntPtr.Zero && IsWindow(hwnd))
{
if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
activated = true;
}
if (!activated && !string.IsNullOrEmpty(path))
{
try {
var fileName = Path.GetFileNameWithoutExtension(path);
var procs = Process.GetProcessesByName(fileName);
foreach (var p in procs) {
if (p.MainWindowHandle != IntPtr.Zero) {
if (IsIconic(p.MainWindowHandle)) ShowWindow(p.MainWindowHandle, SW_RESTORE);
SetForegroundWindow(p.MainWindowHandle);
break;
}
}
} catch { }
}
}
private void DetectSystemTheme()
{
try {
using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")) {
if (key != null) {
object val = key.GetValue("AppsUseLightTheme");
if (val is int && (int)val == 0) _isDarkTheme = true;
}
}
} catch { _isDarkTheme = false; }
}
private void LoadThemeResources()
{
Func<string, SolidColorBrush> hex = (h) => new SolidColorBrush((Color)ColorConverter.ConvertFromString(h));
if (_isDarkTheme)
{
this.Resources["BgBrush"] = hex("#202020");
this.Resources["BorderBrush"] = hex("#383838");
this.Resources["CardBg"] = hex("#2D2D2D");
this.Resources["CardHover"] = hex("#454545");
this.Resources["CardSelected"] = hex("#505050");
this.Resources["TextMain"] = hex("#E0E0E0");
this.Resources["TextSub"] = hex("#A0A0A0");
this.Resources["TextSearch"] = hex("#FFFFFF");
this.Resources["SearchBg"] = hex("#2D2D2D");
this.Resources["IconColor"] = hex("#CCCCCC");
this.Resources["StarColor"] = hex("#FFD700");
this.Resources["ActiveTab"] = hex("#FFFFFF");
this.Resources["InactiveTab"] = hex("#888888");
}
else
{
this.Resources["BgBrush"] = hex("#F7F9FC");
this.Resources["BorderBrush"] = hex("#E0E0E0");
this.Resources["CardBg"] = hex("#FFFFFF");
this.Resources["CardHover"] = hex("#E8F0FE");
this.Resources["CardSelected"] = hex("#D0E0FD");
this.Resources["TextMain"] = hex("#333333");
this.Resources["TextSub"] = hex("#AAAAAA");
this.Resources["TextSearch"] = hex("#333333");
this.Resources["SearchBg"] = hex("#FFFFFF");
this.Resources["IconColor"] = hex("#999999");
this.Resources["StarColor"] = hex("#FFB900");
this.Resources["ActiveTab"] = hex("#333333");
this.Resources["InactiveTab"] = hex("#999999");
}
}
private void OnWindowKeyDown(object sender, KeyEventArgs e)
{
Log($"[OnWindowKeyDown] 按键: {e.Key}");
// 1. 全局 ESC 关闭
if (e.Key == Key.Escape)
{
HideWindow();
e.Handled = true;
return;
}
// 2. 搜索框内按键处理
if (e.OriginalSource == _searchBox)
{
// 在搜索框里按 Down 直接跳列表
if (e.Key == Key.Down)
{
if (_listBox.Items.Count > 0) {
_listBox.SelectedIndex = 0;
_listBox.Focus();
_listBox.ScrollIntoView(_listBox.Items[0]);
var item = _listBox.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem;
if(item != null) item.Focus();
e.Handled = true;
}
}
return;
}
// 3. 极速键盘导航 (WASD模式 + 空格选中)
if (Keyboard.Modifiers == ModifierKeys.None && !(e.OriginalSource is TextBox))
{
bool handled = true;
switch(e.Key)
{
case Key.W: // 上
if (_listBox.SelectedIndex > 0) {
_listBox.SelectedIndex--;
_listBox.ScrollIntoView(_listBox.SelectedItem);
ShowSelectedItemToolTip();
}
break;
case Key.S: // 下
if (_listBox.SelectedIndex < _listBox.Items.Count - 1) {
_listBox.SelectedIndex++;
_listBox.ScrollIntoView(_listBox.SelectedItem);
ShowSelectedItemToolTip();
}
break;
case Key.A: // 左翻页 (全部)
if (IsFavMode) SwitchTab(false);
break;
case Key.D: // 右翻页 (收藏)
if (!IsFavMode) SwitchTab(true);
break;
case Key.Space: // 空格选中
case Key.Enter: // 回车选中
if (_listBox.SelectedItem is ClipItem item) CopyAndClose(item);
break;
case Key.P: // P键置顶 (保留一个原快捷功能)
if (_listBox.SelectedItem is ClipItem pItem) {
pItem.IsPinned = !pItem.IsPinned;
MoveItemToTop(pItem);
SaveData();
if (_showFavoritesOnly && !pItem.IsFavorite) _itemsView.Refresh();
}
break;
default:
handled = false;
break;
}
if (handled) {
e.Handled = true;
return;
}
}
// 4. 数字键快速选择 (1-9)
if ((e.Key >= Key.D1 && e.Key <= Key.D9) || (e.Key >= Key.NumPad1 && e.Key <= Key.NumPad9))
{
if (Keyboard.Modifiers == ModifierKeys.None)
{
int index = (e.Key >= Key.D1 && e.Key <= Key.D9) ? (e.Key - Key.D1) : (e.Key - Key.NumPad1);
if (index < _listBox.Items.Count)
{
var item = _listBox.Items[index] as ClipItem;
if (item != null)
{
CopyAndClose(item);
e.Handled = true;
return;
}
}
}
}
// 5. 其他功能键
if (e.Key == Key.Q)
{
var item = _listBox.SelectedItem as ClipItem;
if (item != null && item.Type == ClipType.Text)
{
ShowQrCode(item.Text);
e.Handled = true;
return;
}
}
// Debug
/*if (e.Key == Key.D && Keyboard.Modifiers == ModifierKeys.Control)
{
// ShowDebugLogs();
e.Handled = true;
return;
}*/ // D键被占用了,Debug可以先移除或者换键
if (e.Key == Key.Delete) {
var item = _listBox.SelectedItem as ClipItem;
if (item != null) { _items.Remove(item); UpdateIndices(); SaveData(); }
}
}
private void OnWindowKeyUp(object sender, KeyEventArgs e)
{
// 全局钩子已经处理了快速选择模式的KeyUp,这里不需要再处理
// 保留此方法但不做任何操作,避免重复触发
}
private void OnWindowMouseWheel(object sender, MouseWheelEventArgs e)
{
// 全局钩子已经处理了快速选择模式的滚轮,这里不需要再处理
}
private void OnTimerTick(object sender, EventArgs e)
{
Perf.Measure("TimerTick", () => {
_tickCounter++;
if (_tickCounter >= 10)
{
CheckClipboardAndAdd();
_tickCounter = 0;
}
}, 30); // Timer整体允许比内部稍长
}
private void CheckClipboardAndAdd(bool silent = false)
{
uint currentSeq = GetClipboardSequenceNumber();
if (currentSeq == _lastClipboardSequenceNumber) return;
// 调试:观察到变化
// Log($"[Debug] 剪贴板序号变化: {_lastClipboardSequenceNumber} -> {currentSeq}");
// 为了测试观测,将阈值降至 1ms
Perf.Measure("CheckClipboardAndAdd", () => {
try
{
if (currentSeq != _lastClipboardSequenceNumber)
{
Log($"[CheckClipboardAndAdd] 检出剪贴板变化: {_lastClipboardSequenceNumber} -> {currentSeq}");
_lastClipboardSequenceNumber = currentSeq;
}
if (!Clipboard.ContainsText() && !Clipboard.ContainsImage() && !Clipboard.ContainsFileDropList()) return;
string currentSig = "";
// 测量获取数据对象耗时
IDataObject data = null;
Perf.Measure("Clipboard.GetDataObject", () => { data = Clipboard.GetDataObject(); }, 10);
if (data == null) return;
string text = null;
if (data.GetDataPresent(DataFormats.UnicodeText)) text = data.GetData(DataFormats.UnicodeText) as string;
if (!string.IsNullOrWhiteSpace(text))
{
text = text.Trim();
currentSig = "TXT:" + text.GetHashCode() + text.Length;
if (currentSig == _lastClipboardSignature) return;
if (silent) { _lastClipboardSignature = currentSig; return; }
if (_items.Count > 0 && _items[0].Text == text) {
_lastClipboardSignature = currentSig;
return;
}
var newItem = new ClipItem {
Type = ClipType.Text,
Text = text,
Timestamp = DateTime.Now,
SourcePath = GetProcessPath(GetForegroundWindow())
};
Perf.Measure("AddNewItem_Text", () => AddNewItem(newItem));
_lastClipboardSignature = currentSig;
}
else if (Clipboard.ContainsImage())
{
// 核心优化:将图像处理移至后台
var bmp = Clipboard.GetImage();
if (bmp != null)
{
// 必须 Freeze 才能跨线程访问
bmp.Freeze();
string lastSig = _lastClipboardSignature;
string cacheDir = _imageCacheDir;
Task.Run(() => {
Perf.Measure("ProcessImageBackground", () => {
try {
string hash = GetBitmapHash(bmp);
string currentSig = "IMG:" + hash;
// 检查重复(后台检查)
bool isDuplicate = false;
this.Dispatcher.Invoke(() => {
if (currentSig == _lastClipboardSignature) isDuplicate = true;
else if (_items.Count > 0 && _items[0].Type == ClipType.Image && _items[0].ImageHash == hash) isDuplicate = true;
});
if (isDuplicate) return;
if (silent) {
this.Dispatcher.Invoke(() => _lastClipboardSignature = currentSig);
return;
}
string fileName = DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".png";
string path = Path.Combine(cacheDir, fileName);
Perf.Measure("SaveImageFile", () => {
using (var fs = new FileStream(path, FileMode.Create))
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
encoder.Save(fs);
}
}, 1);
this.Dispatcher.Invoke(() => {
var newItem = new ClipItem {
Type = ClipType.Image,
Text = "",
DataValue = path,
ImageHash = hash,
Timestamp = DateTime.Now,
SourcePath = GetProcessPath(GetForegroundWindow())
};
AddNewItem(newItem);
_lastClipboardSignature = currentSig;
});
} catch (Exception ex) {
Log("Background Image Processing Error: " + ex.Message);
}
}, 15);
});
}
}
else if (Clipboard.ContainsFileDropList())
{
var files = Clipboard.GetFileDropList();
if (files.Count > 0)
{
string joined = string.Join("|", files.Cast<string>());
currentSig = "FILE:" + joined.GetHashCode();
if (currentSig == _lastClipboardSignature) return;
if (silent) { _lastClipboardSignature = currentSig; return; }
var newItem = new ClipItem {
Type = ClipType.File,
Text = string.Join(", ", files.Cast<string>().Select(Path.GetFileName)), // 去掉 [文件]
DataValue = joined,
Timestamp = DateTime.Now,
SourcePath = GetProcessPath(GetForegroundWindow())
};
AddNewItem(newItem);
_lastClipboardSignature = currentSig;
}
}
}
catch (Exception ex)
{
Log("CheckClipboardAndAdd Error: " + ex.Message);
}
}, 15); // 恢复 15ms 阈值
}
private void AddNewItem(ClipItem item)
{
Log($"[AddNewItem] 新增项: {item.Text?.Substring(0, Math.Min(20, item.Text?.Length ?? 0))}...");
Perf.Measure("AddNewItem_" + item.Type, () => {
// 新条目插入到所有置顶项之后
int insertIndex = _items.Count(x => x.IsPinned);
_items.Insert(insertIndex, item);
for (int i = insertIndex + 1; i < _items.Count; i++)
{
bool duplicate = false;
if (item.Type == ClipType.Text && _items[i].Type == ClipType.Text && _items[i].Text == item.Text) duplicate = true;
else if (item.Type == ClipType.Image && _items[i].Type == ClipType.Image && item.DataValue == _items[i].DataValue) duplicate = true;
if (duplicate && !_items[i].IsPinned && !_items[i].IsFavorite)
{
_items.RemoveAt(i);
i--;
}
}
while (_items.Count > 100)
{
var last = _items.LastOrDefault(x => !x.IsPinned && !x.IsFavorite);
if (last != null) _items.Remove(last);
else break;
}
UpdateIndices();
SaveData();
}, 1);
}
private void LoadSettings()
{
try {
if (File.Exists(_settingsFile))
{
var lines = File.ReadAllLines(_settingsFile);
foreach (var line in lines)
{
var parts = line.Split('=');
if (parts.Length != 2) continue;
var key = parts[0].Trim();
var val = parts[1].Trim();
if (key == "HotkeyCtrl") _hotkeyCtrl = val == "1";
else if (key == "HotkeyShift") _hotkeyShift = val == "1";
else if (key == "HotkeyAlt") _hotkeyAlt = val == "1";
else if (key == "HotkeyKey") int.TryParse(val, out _hotkeyKey);
else if (key == "MouseAtFirst") _mouseActivateAtFirstItem = val == "1";
}
}
} catch { }
}
private void SaveSettings()
{
try {
var lines = new[] {
$"HotkeyCtrl={(_hotkeyCtrl ? "1" : "0")}",
$"HotkeyShift={(_hotkeyShift ? "1" : "0")}",
$"HotkeyAlt={(_hotkeyAlt ? "1" : "0")}",
$"HotkeyAlt={(_hotkeyAlt ? "1" : "0")}",
$"HotkeyKey={_hotkeyKey}",
$"MouseAtFirst={(_mouseActivateAtFirstItem ? "1" : "0")}"
};
File.WriteAllLines(_settingsFile, lines);
} catch { }
}
private void ShowSettingsDialog()
{
_isModalOpen = true;
var win = new Window {
Title = "设置",
Width = 350,
Height = 250,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
WindowStyle = WindowStyle.ToolWindow,
ResizeMode = ResizeMode.NoResize,
Background = (Brush)this.FindResource("BgBrush")
};
var grid = new Grid { Margin = new Thickness(20) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
// 快捷键标签
var label = new TextBlock {
Text = "快速粘贴快捷键:",
Foreground = (Brush)this.FindResource("TextMain"),
Margin = new Thickness(0, 0, 0, 10)
};
Grid.SetRow(label, 0);
grid.Children.Add(label);
// 修饰键选择
var modPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 10) };
var chkCtrl = new CheckBox { Content = "Ctrl", IsChecked = _hotkeyCtrl, Foreground = (Brush)this.FindResource("TextMain"), Margin = new Thickness(0, 0, 15, 0) };
var chkShift = new CheckBox { Content = "Shift", IsChecked = _hotkeyShift, Foreground = (Brush)this.FindResource("TextMain"), Margin = new Thickness(0, 0, 15, 0) };
var chkAlt = new CheckBox { Content = "Alt", IsChecked = _hotkeyAlt, Foreground = (Brush)this.FindResource("TextMain"), Margin = new Thickness(0, 0, 15, 0) };
modPanel.Children.Add(chkCtrl);
modPanel.Children.Add(chkShift);
modPanel.Children.Add(chkAlt);
// 主键选择
var keyLabel = new TextBlock { Text = " + ", Foreground = (Brush)this.FindResource("TextMain"), VerticalAlignment = VerticalAlignment.Center };
modPanel.Children.Add(keyLabel);
var keyCombo = new ComboBox { Width = 60, SelectedIndex = 0 };
for (char c = 'A'; c <= 'Z'; c++) keyCombo.Items.Add(c.ToString());
keyCombo.SelectedIndex = _hotkeyKey - 0x41; // A=0x41
if (keyCombo.SelectedIndex < 0) keyCombo.SelectedIndex = 2; // 默认C
modPanel.Children.Add(keyCombo);
Grid.SetRow(modPanel, 1);
grid.Children.Add(modPanel);
// 中间面板:新选项 + 提示
var midPanel = new StackPanel();
var chkMouse = new CheckBox {
Content = "在鼠标处激活窗口 (首项对齐鼠标)",
IsChecked = _mouseActivateAtFirstItem,
Foreground = (Brush)this.FindResource("TextMain"),
Margin = new Thickness(0, 0, 0, 10),
FontSize = 12
};
midPanel.Children.Add(chkMouse);
// 提示
var tip = new TextBlock {
Text = "按住快捷键呼出窗口,滚动鼠标选择后松开即可粘贴",
Foreground = (Brush)this.FindResource("TextSub"),
FontSize = 11,
TextWrapping = TextWrapping.Wrap
};
midPanel.Children.Add(tip);
Grid.SetRow(midPanel, 2);
grid.Children.Add(midPanel);
// 按钮
var btnPanel = new Grid { Margin = new Thickness(0, 15, 0, 0) };
var linksPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
var openFolder = new TextBlock {
Text = "数据文件夹",
Foreground = (Brush)this.FindResource("TextSub"),
FontSize = 10,
Cursor = Cursors.Hand,
TextDecorations = TextDecorations.Underline,
Margin = new Thickness(0, 0, 10, 0)
};
openFolder.MouseLeftButtonUp += (s, e) => {
try { Process.Start("explorer.exe", _appDataPath); } catch { }
};
linksPanel.Children.Add(openFolder);
var openLog = new TextBlock {
Text = "查看日志",
Foreground = (Brush)this.FindResource("TextSub"),
FontSize = 10,
Cursor = Cursors.Hand,
TextDecorations = TextDecorations.Underline
};
openLog.MouseLeftButtonUp += (s, e) => {
try {
string dir = Directory.GetCurrentDirectory();
if (dir.ToLower().Contains("system32") || dir.ToLower().Contains("program files"))
dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Quicker_ClipFlow");
string path = Path.Combine(dir, "detailed_debug.log");
if (File.Exists(path)) Process.Start("notepad.exe", path);
else MessageBox.Show("日志文件尚不存在,请先产生一些操作。");
} catch { }
};
linksPanel.Children.Add(openLog);
btnPanel.Children.Add(linksPanel);
var actions = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
var btnSave = new Button { Content = "保存", Width = 60, Margin = new Thickness(0, 0, 10, 0) };
var btnCancel = new Button { Content = "取消", Width = 60 };
actions.Children.Add(btnSave);
actions.Children.Add(btnCancel);
btnPanel.Children.Add(actions);
btnSave.Click += (s, e) => {
_hotkeyCtrl = chkCtrl.IsChecked == true;
_hotkeyShift = chkShift.IsChecked == true;
_hotkeyAlt = chkAlt.IsChecked == true;
_hotkeyKey = 0x41 + keyCombo.SelectedIndex;
_mouseActivateAtFirstItem = chkMouse.IsChecked == true;
SaveSettings();
win.Close();
};
btnCancel.Click += (s, e) => win.Close();
Grid.SetRow(btnPanel, 3);
grid.Children.Add(btnPanel);
win.Content = grid;
win.ShowDialog();
_isModalOpen = false;
}
private void LoadData()
{
Perf.Measure("LoadData", () => {
_items = new ObservableCollection<ClipItem>();
if (File.Exists(_dataFile))
{
try {
var xs = new XmlSerializer(typeof(List<ClipItem>));
using (var sr = new StreamReader(_dataFile))
{
var list = (List<ClipItem>)xs.Deserialize(sr);
foreach (var item in list)
{
item.UpdateCategory();
_items.Add(item);
}
}
} catch { }
}
else { }
_itemsView = CollectionViewSource.GetDefaultView(_items);
_itemsView.Filter = ItemFilter;
_listBox.ItemsSource = _itemsView;
// 监听视图变化,自动更新序号
if (_itemsView is INotifyCollectionChanged viewChanged)
{
viewChanged.CollectionChanged += (s, e) => { UpdateIndices(); };
}
UpdateIndices();
}, 1);
// 启动后异步拉取云端收藏 (延迟2秒执行,避免影响启动速度和网络拥塞)
Task.Delay(2000).ContinueWith(t => PullFavoritesFromCloud());
}
// 拉取重试次数
private int _pullRetryCount = 0;
private void PullFavoritesFromCloud()
{
SetSyncState(true);
try
{
Log("[CloudSync] 开始拉取云端收藏...");
string json = CloudStateHelper.ReadText("ClipFlow_Favorites");
if (string.IsNullOrEmpty(json))
{
// 如果是首次拉取失败(非空数据而是读取失败),尝试重试一次
if (_pullRetryCount < 1)
{
_pullRetryCount++;
Log("[CloudSync] 读取失败,5秒后重试...");
SetSyncState(false);
Task.Delay(5000).ContinueWith(t => PullFavoritesFromCloud());
return;
}
Log("[CloudSync] 云端无数据或读取失败");
return;
}
var cloudItems = Newtonsoft.Json.JsonConvert.DeserializeObject<List<CloudFavoriteItem>>(json);
if (cloudItems == null || cloudItems.Count == 0)
{
Log("[CloudSync] 云端收藏为空");
return;
}
Log($"[CloudSync] 云端有 {cloudItems.Count} 条收藏");
// 获取本地已有的文本集合(用于去重)
var localTexts = new HashSet<string>();
this.Dispatcher.Invoke(() => {
foreach (var item in _items)
{
if (item.Type == ClipType.Text && !string.IsNullOrEmpty(item.Text))
localTexts.Add(item.Text);
}
});
int addedCount = 0;
foreach (var cloudItem in cloudItems)
{
if (string.IsNullOrEmpty(cloudItem.Text)) continue;
// 跳过本地已存在的
if (localTexts.Contains(cloudItem.Text)) continue;
// 添加到本地
this.Dispatcher.Invoke(() => {
var newItem = new ClipItem
{
Text = cloudItem.Text,
Type = ClipType.Text,
IsFavorite = true,
IsPinned = cloudItem.IsPinned,
Timestamp = DateTime.TryParse(cloudItem.Time, out var dt) ? dt : DateTime.Now
};
newItem.UpdateCategory();
_items.Insert(0, newItem);
});
localTexts.Add(cloudItem.Text); // 防止重复添加
addedCount++;
}
if (addedCount > 0)
{
Log($"[CloudSync] 从云端恢复 {addedCount} 条收藏,准备同步到 UI...");
this.Dispatcher.Invoke(() => {
UpdateIndices();
if (_itemsView != null) _itemsView.Refresh();
SaveData(); // 保存到本地文件
Log($"[CloudSync] UI 同步完成,当前列表总数: {_items.Count}");
});
}
else
{
Log("[CloudSync] 无新项目需要合并到本地");
}
}
catch (Exception ex)
{
Log($"[CloudSync] 拉取异常: {ex.Message}");
}
finally
{
SetSyncState(false);
}
}
// 云端收藏项的 DTO
private class CloudFavoriteItem
{
public string Text { get; set; }
public string Time { get; set; }
public bool IsPinned { get; set; }
}
private void SaveData()
{
// 改为后台执行,避免阻塞 UI 线程 (日志显示此处耗时 20-50ms)
var itemsCopy = _items.ToList();
string dataFile = _dataFile;
Task.Run(() => {
lock (_saveLock) // 确保同一时间只有一个后台线程在写文件
{
Perf.Measure("SaveData", () => {
try {
var xs = new XmlSerializer(typeof(List<ClipItem>));
string tempFile = dataFile + ".tmp";
Perf.Measure("SerializeOps", () => {
using (var sw = new StreamWriter(tempFile, false, System.Text.Encoding.UTF8))
{
xs.Serialize(sw, itemsCopy);
}
}, 1);
if (File.Exists(dataFile)) File.Delete(dataFile);
File.Move(tempFile, dataFile);
Perf.LogStats("SaveDataSuccess"); // 记录保存后的资源占用
} catch (Exception ex) {
Log("SaveData Error: " + ex.Message);
}
}, 1);
}
// 注意:云同步不在这里触发,而是在收藏状态变化时触发
});
}
// 触发收藏云同步(防抖延迟执行,确保最终状态上传)
private DispatcherTimer _cloudSyncTimer;
private void TriggerFavoriteCloudSync()
{
Log("[CloudSync] 准备同步收藏状态...");
if (_cloudSyncTimer == null)
{
_cloudSyncTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
_cloudSyncTimer.Tick += (s, e) => {
_cloudSyncTimer.Stop();
var itemsCopy = _items.ToList();
Task.Run(() => SyncFavoritesToCloud(itemsCopy));
};
}
_cloudSyncTimer.Stop();
_cloudSyncTimer.Start();
}
// 强制刷新 UI 列表显示
public void RefreshCollectionView()
{
this.Dispatcher.Invoke(() => {
UpdateIndices();
if (_itemsView != null) {
_itemsView.Refresh();
Log($"[UI] 视图已刷新,当前总项数: {_items.Count}");
}
});
}
// 设置同步状态图标显示/隐藏
private void SetSyncState(bool isSyncing)
{
this.Dispatcher.Invoke(() => {
if (_syncIcon == null) return;
if (isSyncing)
{
_syncIcon.Visibility = Visibility.Visible;
var tag = _syncIcon.Tag as dynamic;
if (tag != null)
{
(tag.Transform as RotateTransform)?.BeginAnimation(RotateTransform.AngleProperty, tag.Animation as DoubleAnimation);
}
}
else
{
_syncIcon.Visibility = Visibility.Collapsed;
var tag = _syncIcon.Tag as dynamic;
if (tag != null)
{
(tag.Transform as RotateTransform)?.BeginAnimation(RotateTransform.AngleProperty, null); // 停止动画
}
}
});
}
private static readonly object _cloudSyncLock = new object();
private void SyncFavoritesToCloud(List<ClipItem> items)
{
SetSyncState(true);
lock (_cloudSyncLock)
{
try
{
// 筛选收藏的文本项
var favTexts = items
.Where(i => i.IsFavorite && i.Type == ClipType.Text && !string.IsNullOrEmpty(i.Text))
.Select(i => new {
Text = i.Text,
Time = i.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
IsPinned = i.IsPinned
})
.ToList();
// 即使 favTexts 数量为 0 也要上传(表示清空了云端收藏)
// 序列化为 JSON
var json = Newtonsoft.Json.JsonConvert.SerializeObject(favTexts);
Log($"[CloudSync] 开始上传 {favTexts.Count} 条收藏文本...");
// 上传到云端(key 固定,不设过期)
bool success = CloudStateHelper.SaveText("ClipFlow_Favorites", json);
Log($"[CloudSync] 上传结果: {(success ? "成功" : "失败")}");
}
catch (Exception ex)
{
Log($"[CloudSync] 同步异常: {ex.Message}");
}
finally
{
SetSyncState(false);
}
}
}
private bool ItemFilter(object item)
{
var clip = item as ClipItem;
if (clip == null) return false;
// 收藏标签页:只显示收藏项
// 全部标签页:隐藏所有收藏项(仅作为未归档流水账)
bool matchTab = _showFavoritesOnly ? clip.IsFavorite : !clip.IsFavorite;
bool matchSearch = string.IsNullOrEmpty(_searchText) ||
(!string.IsNullOrEmpty(clip.Text) && clip.Text.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0);
bool matchCategory = _selectedCategory == ContentCategory.General || clip.Category == _selectedCategory;
return matchTab && matchSearch && matchCategory;
}
private void CopyAndClose(ClipItem item)
{
if (item == null) return;
Log($"[CopyAndClose] 点击粘贴. 内容: {item.Text?.Substring(0, Math.Min(20, item.Text?.Length ?? 0))}...");
// 记录调用堆栈,找出是谁触发的粘贴
var stack = Environment.StackTrace;
DebugLog($"[CopyAndClose] 被调用,内容: {item.Text?.Substring(0, Math.Min(20, item.Text?.Length ?? 0))}");
foreach (var line in stack.Split('\n').Skip(1).Take(5))
DebugLog($" {line.Trim()}");
try
{
if (item.Type == ClipType.Text)
{
Clipboard.SetText(item.Text);
}
else if (item.Type == ClipType.Image && File.Exists(item.DataValue))
{
var img = new BitmapImage(new Uri(item.DataValue));
Clipboard.SetImage(img);
}
else if (item.Type == ClipType.File)
{
var list = new System.Collections.Specialized.StringCollection();
foreach(var f in item.DataValue.Split('|')) list.Add(f);
Clipboard.SetFileDropList(list);
}
_lastClipboardSignature = "IGNORE_ONCE";
// 如果是收藏项或置顶项,复制时不移动位置,保持排序
if (!item.IsFavorite && !item.IsPinned)
{
_items.Remove(item);
// 插入到所有置顶项之后
int insertIndex = _items.Count(x => x.IsPinned);
_items.Insert(insertIndex, item);
UpdateIndices();
SaveData();
}
// 如果没钉住窗口,则关闭/隐藏
if (!IsPinnedMode)
{
HideWindow();
}
// 智能激活窗口逻辑
// 如果捕获的句柄有效,且不属于 Quicker 自身(这步很难精准判断,但可以尝试激活)
// 更好的策略是:如果是点击 Quicker 面板启动的,我们只要 HideWindow,系统通常会自动激活 Z-Order 下一层的窗口(即之前的目标窗口)
// 所以我们只在 _targetWindow 看起来非常靠谱时才强制激活
if (_targetWindow != IntPtr.Zero && IsWindow(_targetWindow))
{
// 尝试激活,但不阻塞
ActivateApp(null, _targetWindow);
}
// 异步发送按键,给予窗口切换足够的缓冲时间
new Task(async () => {
await Task.Delay(250); // 增加延时到 250ms,确保焦点已切换
// 发送 Ctrl+V
keybd_event(17, 0, 0, UIntPtr.Zero); // Ctrl Down
keybd_event(86, 0, 0, UIntPtr.Zero); // V Down
keybd_event(86, 0, KEYEVENTF_KEYUP, UIntPtr.Zero); // V Up
keybd_event(17, 0, KEYEVENTF_KEYUP, UIntPtr.Zero); // Ctrl Up
}).Start();
}
catch (Exception ex)
{
MessageBox.Show("复制失败: " + ex.Message);
}
}
private void ShowQrCode(string text)
{
Window qrWin = new Window
{
Title = "文本查看", Width = 400, Height = 300,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
WindowStyle = WindowStyle.ToolWindow,
Background = (Brush)this.FindResource("CardBg")
};
var tb = new TextBox {
Text = text, TextWrapping = TextWrapping.Wrap,
FontSize = 14, Margin = new Thickness(10),
BorderThickness = new Thickness(0), Background = Brushes.Transparent,
Foreground = (Brush)this.FindResource("TextMain"),
IsReadOnly = true,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto
};
qrWin.Content = tb;
_isModalOpen = true;
qrWin.ShowDialog();
_isModalOpen = false;
}
private TextBlock CreateTabButton(string text, bool isActive)
{
return new TextBlock
{
Text = text,
FontSize = 13,
FontWeight = isActive ? FontWeights.Bold : FontWeights.Normal,
Foreground = (Brush)this.FindResource(isActive ? "ActiveTab" : "InactiveTab"),
Cursor = Cursors.Hand,
VerticalAlignment = VerticalAlignment.Center
};
}
private void SwitchTab(bool favOnly)
{
_showFavoritesOnly = favOnly;
// 更新依赖属性状态,驱动 XAML 隐藏/显示拖拽手柄
this.IsFavMode = favOnly;
if (_tabAll != null)
{
_tabAll.Foreground = (Brush)this.FindResource(!favOnly ? "ActiveTab" : "InactiveTab");
_tabAll.FontWeight = !favOnly ? FontWeights.Bold : FontWeights.Normal;
}
if (_tabFav != null)
{
_tabFav.Foreground = (Brush)this.FindResource(favOnly ? "ActiveTab" : "InactiveTab");
_tabFav.FontWeight = favOnly ? FontWeights.Bold : FontWeights.Normal;
}
if (_itemsView != null)
{
_itemsView.Refresh();
}
}
private object GetElementFromPoint(ListBox listbox, Point point)
{
UIElement element = listbox.InputHitTest(point) as UIElement;
while (element != null)
{
if (element == listbox) return null;
object item = listbox.ItemContainerGenerator.ItemFromContainer(element);
if (!item.Equals(DependencyProperty.UnsetValue)) return item;
element = VisualTreeHelper.GetParent(element) as UIElement;
}
return null;
}
private void ListBox_ItemClicked(object sender, MouseButtonEventArgs e)
{
var original = e.OriginalSource as DependencyObject;
if (original == null) return;
// 排除 ToggleButton (收藏/置顶按钮)
if (GetDependencyObjectFromVisualTree(original, typeof(System.Windows.Controls.Primitives.ToggleButton)) != null) return;
// 排除 DragHandle (拖拽手柄)
if (original is FrameworkElement fe && fe.Tag?.ToString() == "DragHandle") return;
// 排除 ScrollBar (由 ListBox 内部处理)
if (GetDependencyObjectFromVisualTree(original, typeof(System.Windows.Controls.Primitives.ScrollBar)) != null) return;
// 获取点击的 ListBoxItem
var container = GetDependencyObjectFromVisualTree(original, typeof(ListBoxItem)) as ListBoxItem;
if (container != null)
{
var item = _listBox.ItemContainerGenerator.ItemFromContainer(container) as ClipItem;
if (item != null)
{
CopyAndClose(item);
}
}
}
private void ListBox_ItemRightClicked(object sender, MouseButtonEventArgs e)
{
var item = GetElementFromPoint(_listBox, e.GetPosition(_listBox)) as ClipItem;
if (item != null)
{
_listBox.SelectedItem = item;
ContextMenu cm = new ContextMenu();
MenuItem miDel = new MenuItem { Header = "删除" };
miDel.Click += (s, args) => {
_items.Remove(item);
UpdateIndices();
SaveData();
};
MenuItem miFav = new MenuItem { Header = item.IsFavorite ? "取消收藏" : "收藏" };
miFav.Click += (s, args) => { item.IsFavorite = !item.IsFavorite; SaveData(); TriggerFavoriteCloudSync(); };
cm.Items.Add(miFav);
cm.Items.Add(miDel);
cm.IsOpen = true;
}
}
private void ListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject obj)
{
var parent = obj;
bool isHandle = false;
// 只要点击的是左侧序号区域或右侧拖动柄,就触发拖拽
while (parent != null && !(parent is ListBoxItem))
{
if (parent is FrameworkElement fe)
{
if (fe.Tag != null && fe.Tag.ToString() == "DragHandle")
{
isHandle = true;
break;
}
}
parent = VisualTreeHelper.GetParent(parent);
}
if (isHandle)
{
var itemContainer = GetDependencyObjectFromVisualTree(obj, typeof(ListBoxItem)) as ListBoxItem;
if (itemContainer != null)
{
var item = _listBox.ItemContainerGenerator.ItemFromContainer(itemContainer) as ClipItem;
if (item != null)
{
var data = new DataObject();
// 内部数据:用于在收藏夹内排序
data.SetData(typeof(ClipItem), item);
// 外部数据:用于拖拽到其他软件粘贴
if (item.Type == ClipType.Text) {
data.SetData(DataFormats.UnicodeText, item.Text);
data.SetData(DataFormats.Text, item.Text); // 增加标准文本格式作为兜底
} else if (item.Type == ClipType.Image || item.Type == ClipType.File) {
var files = new System.Collections.Specialized.StringCollection();
if (item.Type == ClipType.Image) {
if (File.Exists(item.DataValue)) files.Add(Path.GetFullPath(item.DataValue));
} else {
foreach(var f in item.DataValue.Split('|')) {
if (File.Exists(f)) files.Add(Path.GetFullPath(f));
}
}
if (files.Count > 0) {
data.SetFileDropList(files);
// 为记事本等纯文本编辑器增加路径文本输出,防止拒绝拖拽
string pathsText = string.Join(Environment.NewLine, files.Cast<string>());
data.SetData(DataFormats.UnicodeText, pathsText);
data.SetData(DataFormats.Text, pathsText);
}
}
DragDrop.DoDragDrop(itemContainer, data, DragDropEffects.Copy | DragDropEffects.Move);
e.Handled = true;
}
}
}
}
}
private DependencyObject GetDependencyObjectFromVisualTree(DependencyObject startObject, Type type)
{
var parent = startObject;
while (parent != null)
{
if (type.IsInstanceOfType(parent)) return parent;
parent = VisualTreeHelper.GetParent(parent);
}
return null;
}
private void ListBox_Drop(object sender, DragEventArgs e)
{
// 内部排序只在收藏夹模式下生效
if (!_showFavoritesOnly) return;
var droppedData = e.Data.GetData(typeof(ClipItem)) as ClipItem;
var target = GetElementFromPoint(_listBox, e.GetPosition(_listBox)) as ClipItem;
if (droppedData != null && target != null && droppedData != target)
{
int oldIdx = _items.IndexOf(droppedData);
int newIdx = _items.IndexOf(target);
if (oldIdx != -1 && newIdx != -1)
{
_items.Move(oldIdx, newIdx);
UpdateIndices();
SaveData();
}
}
}
private void OnToggleButtonClick(object sender, RoutedEventArgs e)
{
var btn = e.OriginalSource as System.Windows.Controls.Primitives.ToggleButton;
if (btn != null)
{
var item = btn.DataContext as ClipItem;
if (item != null)
{
string tag = btn.Tag as string;
if (tag == "FavBtn")
{
// 收藏按钮点击
Log($"[UI] 收藏按钮点击: IsFavorite={btn.IsChecked}");
item.IsFavorite = btn.IsChecked == true;
// 收藏状态改变会导致 item 进出当前 Filter,需要刷新视图
_itemsView.Refresh();
SaveData();
TriggerFavoriteCloudSync(); // 收藏变化时触发云同步
}
else if (tag == "PinBtn")
{
// 置顶按钮点击
Log($"[UI] 置顶按钮点击: IsPinned={btn.IsChecked}");
MoveItemToTop(item);
SaveData();
}
}
}
e.Handled = true;
}
private void OnWindowDeactivated(object sender, EventArgs e)
{
if (_isModalOpen) return;
if (IsPinnedMode) return;
if (_isQuickSelectMode) return;
if (_quickModeJustExited) return;
// 增加保护:如果窗口刚显示不到 1000ms,忽略失焦隐藏
// 解决因激活瞬间焦點争夺导致的闪退
var duration = (DateTime.Now - _lastShowTime).TotalMilliseconds;
if (this.Visibility == Visibility.Visible && duration < 1000) return;
try {
if (this.OwnedWindows.Count == 0) HideWindow();
} catch {}
}
private void OnDeactivated(object sender, EventArgs e)
{
OnWindowDeactivated(sender, e);
}
private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e) {
if (!_forceClose)
{
e.Cancel = true;
HideWindow();
}
else
{
CleanupResources();
}
}
private void CleanupResources()
{
try {
SaveData();
SaveWindowPosition();
} catch { }
try {
if (_monitorTimer != null)
{
_monitorTimer.Stop();
_monitorTimer = null;
}
} catch { }
try {
if (_searchTimer != null)
{
_searchTimer.Stop();
_searchTimer = null;
}
} catch { }
try {
if (_tooltipTimer != null)
{
_tooltipTimer.Stop();
_tooltipTimer = null;
}
} catch { }
try {
UninstallGlobalHooks();
} catch { }
}
~ClipboardHistoryWindow()
{
// 析构函数作为保险,确保钩子被卸载
try { UninstallGlobalHooks(); } catch { }
}
private void UpdateIndices()
{
// 先将所有项的序号置空
foreach (var item in _items) item.Index = 0;
int idx = 1;
// 仅对当前视图中可见的项进行编号
if (_itemsView != null)
{
foreach (var item in _itemsView)
{
if (item is ClipItem clip)
{
clip.Index = idx++;
}
}
}
else
{
// Fallback: 如果视图未初始化,则按过滤逻辑模拟编号
foreach (var item in _items)
{
if (ItemFilter(item))
{
item.Index = idx++;
}
}
}
}
private void MoveItemToTop(ClipItem item)
{
if (item == null) return;
int oldIndex = _items.IndexOf(item);
if (oldIndex < 0) return;
int newIndex = 0;
if (item.IsPinned)
{
newIndex = 0;
}
else
{
newIndex = _items.Count(x => x.IsPinned && x != item);
}
if (oldIndex != newIndex)
{
_items.Move(oldIndex, newIndex);
}
UpdateIndices();
}
private void InitializeUI()
{
var outerBorder = new Border
{
Background = (Brush)this.FindResource("BgBrush"),
CornerRadius = new CornerRadius(12),
BorderBrush = (Brush)this.FindResource("BorderBrush"),
BorderThickness = new Thickness(1),
Padding = new Thickness(0)
};
outerBorder.Effect = new DropShadowEffect { BlurRadius = 25, ShadowDepth = 8, Opacity = 0.4, Color = Colors.Black };
var mainGrid = new Grid();
mainGrid.Margin = new Thickness(15, 10, 15, 10);
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(45) });
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(35) });
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(25) });
// Header
var topBar = new Grid();
topBar.Background = Brushes.Transparent;
topBar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
topBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
topBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var tabsPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
// 移除阻塞,允许子控件接收点击事件
// 全部按钮
_tabAll = CreateTabButton("全部", true);
_tabAll.FontSize = 13;
_tabAll.FontWeight = FontWeights.Bold;
_tabAll.MouseLeftButtonUp += (s, e) => {
_selectedCategory = ContentCategory.General;
SwitchTab(false);
};
tabsPanel.Children.Add(_tabAll);
// 分类下拉图标
var catArrow = new TextBlock {
Text = " ▾",
FontSize = 14,
Foreground = (Brush)this.FindResource("InactiveTab"),
Cursor = Cursors.Hand,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(2, 0, 10, 0)
};
catArrow.MouseLeftButtonUp += (s, e) => {
ContextMenu menu = new ContextMenu();
string[] names = { "全部历史", " 链接", " 邮箱", " 电话", " 代码", " 结构化", " 颜色", " 路径", " 时间", " IP", " 文件", "️ 图片" };
for (int i = 0; i < names.Length; i++) {
int idx = i;
MenuItem mi = new MenuItem { Header = names[i] };
mi.Click += (ms, me) => {
if (idx <= 0) _selectedCategory = ContentCategory.General;
else _selectedCategory = (ContentCategory)idx;
// 切换类别时自动回到“全部”标签
SwitchTab(false);
};
menu.Items.Add(mi);
}
menu.IsOpen = true;
};
tabsPanel.Children.Add(catArrow);
tabsPanel.Children.Add(new Border { Width = 1, Height = 14, Background = (Brush)this.FindResource("IconColor"), Margin = new Thickness(0, 0, 10, 0), Opacity = 0.5 });
_tabFav = CreateTabButton("★ 收藏", false);
_tabFav.FontSize = 13;
_tabFav.FontWeight = FontWeights.Normal;
_tabFav.FontWeight = FontWeights.Normal;
_tabFav.MouseLeftButtonUp += (s, e) => { SwitchTab(true); };
tabsPanel.Children.Add(_tabFav);
// 同步状态图标 (默认隐藏)
_syncIcon = new TextBlock {
Text = "⏳", // 或者使用 ↻
FontSize = 12,
Foreground = (Brush)this.FindResource("TextSub"),
Margin = new Thickness(8, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
Visibility = Visibility.Collapsed,
ToolTip = "正在同步云端数据..."
};
// 定义旋转动画
var rotateTransform = new RotateTransform(0, 5, 6); // 中心点大约在字符中心
_syncIcon.RenderTransform = rotateTransform;
var anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever;
// 存储动画以便按需启动
_syncIcon.Tag = new { Transform = rotateTransform, Animation = anim };
tabsPanel.Children.Add(_syncIcon);
Grid.SetColumn(tabsPanel, 0);
topBar.Children.Add(tabsPanel);
// 修改:右上角替换为“钉住”按钮
_pinWindowBtn = new System.Windows.Controls.Primitives.ToggleButton
{
Content = "", Width = 30, Height = 26, FontSize = 12,
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand,
Foreground = (Brush)this.FindResource("IconColor"),
Margin = new Thickness(0, 0, 10, 0),
ToolTip = "钉住窗口:失去焦点时不自动隐藏"
};
_pinWindowBtn.Checked += (s, e) => { this.Topmost = true; _pinWindowBtn.Foreground = (Brush)this.FindResource("StarColor"); };
_pinWindowBtn.Unchecked += (s, e) => { this.Topmost = false; _pinWindowBtn.Foreground = (Brush)this.FindResource("IconColor"); };
Grid.SetColumn(_pinWindowBtn, 1);
topBar.Children.Add(_pinWindowBtn);
// 关闭按钮
var closeBtn = new Button
{
Content = "✕", Width = 40, Height = 40, FontSize = 18, FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand,
Foreground = (Brush)this.FindResource("IconColor"),
Margin = new Thickness(0, 0, -5, 0)
};
closeBtn.Click += new RoutedEventHandler((s,e) => { HideWindow(); });
Grid.SetColumn(closeBtn, 2);
topBar.Children.Add(closeBtn);
Grid.SetRow(topBar, 0);
mainGrid.Children.Add(topBar);
var searchBorder = new Border {
Background = (Brush)this.FindResource("SearchBg"),
CornerRadius = new CornerRadius(6),
BorderBrush = (Brush)this.FindResource("BorderBrush"),
BorderThickness = new Thickness(1),
Padding = new Thickness(5,0,5,0),
Height = 28,
Margin = new Thickness(2, 0, 2, 8)
};
_searchBox = new TextBox {
BorderThickness = new Thickness(0), Background = Brushes.Transparent,
VerticalContentAlignment = VerticalAlignment.Center, FontSize = 12,
Foreground = (Brush)this.FindResource("TextSearch"),
CaretBrush = (Brush)this.FindResource("TextSearch"),
Height = 26,
Tag = "搜索内容..."
};
_searchBox.TextChanged += (s, e) => {
if (_searchTimer == null)
{
_searchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_searchTimer.Tick += (st, se) => {
_searchTimer.Stop();
_searchText = _searchBox.Text;
if (_itemsView != null) _itemsView.Refresh();
};
}
_searchTimer.Stop();
_searchTimer.Start();
};
searchBorder.Child = _searchBox;
Grid.SetRow(searchBorder, 1);
mainGrid.Children.Add(searchBorder);
// --- List Box Config ---
_listBox = new ListBox();
_listBox.Background = Brushes.Transparent;
_listBox.BorderThickness = new Thickness(0);
_listBox.Padding = new Thickness(2);
_listBox.Style = null;
_listBox.Focusable = true;
string itemStyleXaml = @"
<Style TargetType='ListBoxItem'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Setter Property='Background' Value='Transparent'/>
<Setter Property='BorderThickness' Value='0'/>
<Setter Property='Margin' Value='2,1,2,1'/>
<Setter Property='Padding' Value='0'/>
<Setter Property='FocusVisualStyle' Value='{x:Null}'/>
<Setter Property='HorizontalContentAlignment' Value='Stretch'/>
<Setter Property='Template'>
<Setter.Value>
<ControlTemplate TargetType='ListBoxItem'>
<Border x:Name='Bd' Background='{TemplateBinding Background}'
CornerRadius='10' SnapsToDevicePixels='true' Margin='1,0,1,0'
BorderThickness='0' BorderBrush='Transparent'>
<ContentPresenter HorizontalAlignment='{TemplateBinding HorizontalContentAlignment}'
VerticalAlignment='{TemplateBinding VerticalContentAlignment}'/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property='IsSelected' Value='true'>
<Setter TargetName='Bd' Property='Background' Value='#1e293b'/>
</Trigger>
<Trigger Property='IsMouseOver' Value='true'>
<Setter TargetName='Bd' Property='Background' Value='#2d2d2d'/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property='IsSelected' Value='true'/>
<Condition Property='IsMouseOver' Value='true'/>
</MultiTrigger.Conditions>
<Setter TargetName='Bd' Property='Background' Value='#1e293b'/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>";
try {
_listBox.ItemContainerStyle = (Style)System.Windows.Markup.XamlReader.Parse(itemStyleXaml);
} catch (Exception ex) {
DebugLog("Style Parse Error: " + ex.Message);
}
_listBox.HorizontalContentAlignment = HorizontalAlignment.Stretch;
// 性能优化:启用 UI 虚拟化
VirtualizingPanel.SetIsVirtualizing(_listBox, true);
VirtualizingPanel.SetVirtualizationMode(_listBox, VirtualizationMode.Recycling);
VirtualizingPanel.SetScrollUnit(_listBox, ScrollUnit.Pixel);
_listBox.ItemsPanel = new ItemsPanelTemplate(new FrameworkElementFactory(typeof(VirtualizingStackPanel)));
ScrollViewer.SetHorizontalScrollBarVisibility(_listBox, ScrollBarVisibility.Disabled);
ScrollViewer.SetVerticalScrollBarVisibility(_listBox, ScrollBarVisibility.Hidden);
// 数据模板
string dataTemplateXaml = @"
<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Grid Margin='0,6,12,6' Background='Transparent'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='40'/> <!-- Index -->
<ColumnDefinition Width='Auto'/> <!-- Icon Box -->
<ColumnDefinition Width='*'/> <!-- Content -->
<ColumnDefinition Width='Auto'/> <!-- Actions -->
</Grid.ColumnDefinitions>
<!-- Col 0: Index & Pin -->
<Grid Grid.Column='0' HorizontalAlignment='Center' VerticalAlignment='Center'>
<TextBlock Text='{Binding IndexDisplay}'
VerticalAlignment='Center' HorizontalAlignment='Center'
Foreground='#999999' FontSize='14' Name='IndexTxt' FontFamily='Consolas'/>
<ToggleButton Name='PinBtn' IsChecked='{Binding IsPinned}'
Tag='PinBtn'
Cursor='Hand' Width='30' Height='30'
HorizontalAlignment='Stretch' VerticalAlignment='Stretch'
BorderThickness='0' Background='Transparent' Opacity='0'>
<ToggleButton.Template>
<ControlTemplate TargetType='ToggleButton'>
<Border Background='Transparent' Name='Bg'>
<Path Name='PinImg' Width='14' Height='14' Stretch='Uniform' Fill='White' Visibility='Collapsed'
Data='M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22L12,22.8L12.8,22V16H18V14L16,12Z'/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property='IsMouseOver' Value='True'>
<Setter TargetName='PinImg' Property='Visibility' Value='Visible'/>
</Trigger>
<Trigger Property='IsChecked' Value='True'>
<Setter TargetName='PinImg' Property='Visibility' Value='Visible'/>
<Setter TargetName='PinImg' Property='Fill' Value='#4CAF50'/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Grid>
<!-- Col 1: Icon Box (Rounded Square) -->
<Grid Grid.Column='1' VerticalAlignment='Center' HorizontalAlignment='Left'
Visibility='{Binding CategoryVisibility}' Margin='0,0,12,0'>
<Border Width='34' Height='34' CornerRadius='8' Background='#1a1a1a' BorderBrush='#2d2d2d' BorderThickness='1'>
<TextBlock Text='{Binding CategoryIcon}' FontSize='12' Foreground='{Binding CategoryColor}'
VerticalAlignment='Center' HorizontalAlignment='Center'/>
</Border>
</Grid>
<!-- Col 2: Content -->
<StackPanel Grid.Column='2' VerticalAlignment='Center' Margin='4,0,0,0'
ToolTipService.InitialShowDelay='200'
ToolTipService.BetweenShowDelay='100'
ToolTipService.ShowDuration='30000'
ToolTip='{Binding ToolTipText}'>
<StackPanel Orientation='Horizontal' VerticalAlignment='Center'>
<TextBlock Text='{Binding Text}' Visibility='{Binding TextVisibility}'
FontSize='13' FontFamily='Microsoft YaHei UI' TextTrimming='CharacterEllipsis'
MaxHeight='54' TextWrapping='Wrap' Foreground='{DynamicResource TextMain}'
LineHeight='18' VerticalAlignment='Center'/>
</StackPanel>
<Border CornerRadius='4' Background='Transparent' HorizontalAlignment='Left'
Visibility='{Binding ImageVisibility}' Margin='0,4,0,0'
ToolTipService.InitialShowDelay='200'>
<Border.ToolTip>
<ToolTip Placement='Right'>
<Image Source='{Binding PreviewImagePath}' MaxWidth='500' MaxHeight='400' Stretch='Uniform'/>
</ToolTip>
</Border.ToolTip>
<Image Source='{Binding Thumbnail}' Height='50' Stretch='Uniform'/>
</Border>
</StackPanel>
<!-- Col 3: Actions & Time (Hover states) -->
<Grid Grid.Column='3' VerticalAlignment='Center' HorizontalAlignment='Right'>
<TextBlock Name='TimeText' Text='{Binding FriendlyTime}' FontSize='9' Foreground='#999999' Margin='0,0,2,0'/>
<StackPanel Name='HoverBtns' Orientation='Horizontal' Visibility='Collapsed'>
<ToggleButton Name='FavBtn' IsChecked='{Binding IsFavorite}' Cursor='Hand'
Tag='FavBtn'
Background='Transparent' BorderThickness='0' Width='20' Height='20' Margin='0,0,5,0'>
<ToggleButton.Template>
<ControlTemplate TargetType='ToggleButton'>
<Path Name='Star' Width='14' Height='14' Stretch='Uniform' Fill='#444444' Data='M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z'/>
<ControlTemplate.Triggers>
<Trigger Property='IsChecked' Value='True'>
<Setter TargetName='Star' Property='Fill' Value='#facc15'/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Border Width='20' Height='20' Background='Transparent' Cursor='SizeNS' Name='DragHandle' Tag='DragHandle' ToolTip='按住排序'>
<TextBlock Text='⋮⋮' FontSize='12' Foreground='#444444' VerticalAlignment='Center' HorizontalAlignment='Center'/>
</Border>
</StackPanel>
</Grid>
</Grid>
<DataTemplate.Triggers>
<!-- Selection Trigger -->
<DataTrigger Binding='{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}' Value='True'>
<Setter TargetName='IndexTxt' Property='Foreground' Value='#3b82f6'/>
<Setter TargetName='TimeText' Property='Foreground' Value='#3b82f6'/>
</DataTrigger>
<DataTrigger Binding='{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsMouseOver}' Value='True'>
<Setter TargetName='TimeText' Property='Visibility' Value='Collapsed'/>
<Setter TargetName='HoverBtns' Property='Visibility' Value='Visible'/>
<Setter TargetName='PinBtn' Property='Opacity' Value='1'/>
</DataTrigger>
<Trigger SourceName='PinBtn' Property='IsMouseOver' Value='True'>
<Setter TargetName='IndexTxt' Property='Visibility' Value='Collapsed'/>
</Trigger>
<DataTrigger Binding='{Binding IsPinned}' Value='True'>
<Setter TargetName='IndexTxt' Property='Visibility' Value='Collapsed'/>
<Setter TargetName='PinBtn' Property='Opacity' Value='1'/>
</DataTrigger>
<DataTrigger Binding='{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=IsFavMode}' Value='False'>
<Setter TargetName='DragHandle' Property='Visibility' Value='Collapsed'/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>";
try {
dataTemplateXaml = dataTemplateXaml.Replace("Click='OnPinClick'", "");
_listBox.ItemTemplate = (DataTemplate)XamlReader.Parse(dataTemplateXaml);
} catch (Exception ex) { MessageBox.Show("模板错误: " + ex.Message); }
_listBox.PreviewMouseLeftButtonUp += new MouseButtonEventHandler(ListBox_ItemClicked);
_listBox.PreviewMouseRightButtonUp += new MouseButtonEventHandler(ListBox_ItemRightClicked);
_listBox.PreviewMouseDown += (s, e) => {
if (e.ChangedButton == MouseButton.Middle) {
var item = GetElementFromPoint(_listBox, e.GetPosition(_listBox)) as ClipItem;
if (item != null) { _items.Remove(item); UpdateIndices(); SaveData(); }
}
};
_listBox.PreviewMouseLeftButtonDown += ListBox_PreviewMouseLeftButtonDown;
_listBox.Drop += ListBox_Drop;
_listBox.AllowDrop = true;
_listBox.AddHandler(System.Windows.Controls.Primitives.ToggleButton.ClickEvent, new RoutedEventHandler(OnToggleButtonClick));
Grid.SetRow(_listBox, 2);
mainGrid.Children.Add(_listBox);
// Footer
var footerGrid = new Grid();
footerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
footerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
footerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
footerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var footerTip = new TextBlock {
Text = "左键:复制 · 右键:菜单 · Q:查看 · 中键:删除",
FontSize = 9, Foreground = (Brush)this.FindResource("TextSub"),
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(5,0,0,0)
};
Grid.SetColumn(footerTip, 0);
footerGrid.Children.Add(footerTip);
var settingsBtn = new TextBlock {
Text = "设置", FontSize = 10, Foreground = (Brush)this.FindResource("TextSub"),
Cursor = Cursors.Hand, TextDecorations = TextDecorations.Underline,
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0,0,10,0)
};
settingsBtn.MouseLeftButtonUp += (s, e) => { ShowSettingsDialog(); };
Grid.SetColumn(settingsBtn, 1);
footerGrid.Children.Add(settingsBtn);
var clearBtn = new TextBlock {
Text = "清空", FontSize = 10, Foreground = (Brush)this.FindResource("TextSub"),
Cursor = Cursors.Hand, TextDecorations = TextDecorations.Underline,
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0,0,10,0)
};
clearBtn.MouseLeftButtonUp += (s, e) => {
if (MessageBox.Show("确定清空所有非收藏记录吗?", "确认", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
var keep = _items.Where(x => x.IsFavorite || x.IsPinned).ToList();
_items.Clear();
foreach(var k in keep) _items.Add(k);
// 修复:不要重置签名,保持之前的指纹,防止清空后立刻被监控补回相同内容
UpdateIndices();
SaveData();
CleanupImageCache(true);
if (_itemsView != null) _itemsView.Refresh();
}
};
Grid.SetColumn(clearBtn, 2);
footerGrid.Children.Add(clearBtn);
var exitBtn = new TextBlock {
Text = "退出", FontSize = 10, Foreground = (Brush)this.FindResource("TextSub"),
Cursor = Cursors.Hand, TextDecorations = TextDecorations.Underline,
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0,0,5,0)
};
exitBtn.MouseLeftButtonUp += (s, e) => {
if (MessageBox.Show("确定要退出剪贴板历史工具吗?\n退出后将不再监听剪贴板。", "退出", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Quit();
}
};
Grid.SetColumn(exitBtn, 3);
footerGrid.Children.Add(exitBtn);
Grid.SetRow(footerGrid, 3);
mainGrid.Children.Add(footerGrid);
outerBorder.Child = mainGrid;
this.Content = outerBorder;
}
}
// CloudState Helper (通过反射调用 Quicker 内置云同步 API)
// ---------------------------------------------------------
static class CloudStateHelper
{
private static Type _cloudStateType;
private static object _instance;
private static System.Reflection.MethodInfo _saveMethod, _readMethod;
private static bool _initialized = false;
private static string _initError = null;
static CloudStateHelper() { Init(); }
static void Init()
{
if (_initialized) return;
_initialized = true;
try
{
// 在所有已加载的程序集中查找 CloudState 类型
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
_cloudStateType = assembly.GetType("Quicker.Public.CloudState");
if (_cloudStateType != null) break;
}
if (_cloudStateType == null)
{
_initError = "未找到 CloudState 类型";
return;
}
// 获取单例实例(通过内部字段)
var instanceField = _cloudStateType.GetField("OWJCWqTrDY4Wyj5Oc7t3",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (instanceField != null)
_instance = instanceField.GetValue(null);
if (_instance == null)
{
_instance = Activator.CreateInstance(_cloudStateType, true);
if (instanceField != null)
try { instanceField.SetValue(null, _instance); } catch { }
}
if (_instance == null)
{
_initError = "无法获取 CloudState 实例";
return;
}
// 获取方法引用
_saveMethod = _cloudStateType.GetMethod("SaveTextAsync", new[] { typeof(string), typeof(string), typeof(double) });
_readMethod = _cloudStateType.GetMethod("ReadTextAsync", new[] { typeof(string), typeof(double) });
if (_saveMethod == null) _initError = "未找到 SaveTextAsync 方法";
else if (_readMethod == null) _initError = "未找到 ReadTextAsync 方法";
}
catch (Exception ex)
{
_initError = $"初始化异常: {ex.Message}";
}
}
public static bool SaveText(string key, string value, double expireMinutes = 525600)
{
if (_instance == null || _saveMethod == null) { Init(); }
if (_instance == null || _saveMethod == null)
{
ClipboardHistoryWindow.Log($"[CloudSync] SaveText 初始化失败: {_initError}");
return false;
}
string safeValue = value ?? "";
ClipboardHistoryWindow.Log($"[CloudSync] 调用 SaveTextAsync: key={key}, len={safeValue.Length}, expire={expireMinutes}");
try
{
var task = (System.Threading.Tasks.Task)_saveMethod.Invoke(_instance, new object[] { key, safeValue, expireMinutes });
bool completed = task.Wait(10000); // 等待最多10秒
if (task.IsFaulted)
{
var innerEx = task.Exception?.InnerException ?? task.Exception;
ClipboardHistoryWindow.Log($"[CloudSync] SaveText 任务失败: {innerEx?.Message}");
return false;
}
if (!task.IsCompleted)
{
ClipboardHistoryWindow.Log("[CloudSync] SaveText 超时");
return false;
}
ClipboardHistoryWindow.Log("[CloudSync] SaveText 成功");
return true;
}
catch (Exception ex)
{
var innerEx = ex.InnerException ?? ex;
ClipboardHistoryWindow.Log($"[CloudSync] SaveText 异常: {innerEx.Message}");
return false;
}
}
public static string ReadText(string key, double expireMinutes = 525600)
{
if (_instance == null || _readMethod == null) { Init(); }
if (_instance == null || _readMethod == null)
{
ClipboardHistoryWindow.Log($"[CloudSync] ReadText 初始化失败: {_initError}");
return null;
}
ClipboardHistoryWindow.Log($"[CloudSync] 调用 ReadTextAsync: key={key}");
try
{
var task = (System.Threading.Tasks.Task)_readMethod.Invoke(_instance, new object[] { key, expireMinutes });
task.Wait(30000); // 增加超时到30秒
if (task.IsFaulted)
{
var innerEx = task.Exception?.InnerException ?? task.Exception;
ClipboardHistoryWindow.Log($"[CloudSync] ReadText 任务失败: {innerEx?.Message}");
return null;
}
if (task.IsCompleted)
{
var result = task.GetType().GetProperty("Result")?.GetValue(task) as string;
ClipboardHistoryWindow.Log($"[CloudSync] ReadText 成功, len={result?.Length ?? 0}");
return result;
}
ClipboardHistoryWindow.Log("[CloudSync] ReadText 超时");
return null;
}
catch (Exception ex)
{
var innerEx = ex.InnerException ?? ex;
ClipboardHistoryWindow.Log($"[CloudSync] ReadText 异常: {innerEx.Message}");
return null;
}
}
}
动作配置 (JSON)
{
"ActionId": "85ebb48b-359a-4ad3-8786-d07fb840268f",
"Title": "剪贴板",
"Description": "WASD极速导航 | 收藏云同步 | 临时置顶 | 图片预览 | 一键收藏 | 零秒启动",
"Icon": "https://files.getquicker.net/_icons/84F475C785EF026EF5688569D83B31B347D59DA9.png",
"Variables": [
{
"Type": 0,
"Desc": "结果文本(自动)",
"IsOutput": false,
"Key": "text"
},
{
"Type": 0,
"Desc": "返回值(自动)",
"IsOutput": false,
"Key": "rtn"
},
{
"Type": 0,
"Desc": "错误信息(自动)",
"IsOutput": false,
"Key": "errMessage"
},
{
"Type": 0,
"Desc": "错误详情(自动)",
"IsOutput": false,
"Key": "errorDesc"
},
{
"Type": 2,
"Desc": "后台执行",
"DefaultValue": "false",
"IsInput": false,
"Key": "后台执行线程"
},
{
"Type": 2,
"Desc": "静默启动",
"DefaultValue": "false",
"IsInput": false,
"Key": "silent"
}
],
"References": []
}
使用方法
- 确保您已安装 动作构建 (Action Builder) 动作。
- 将本文提供的
.cs和.json文件下载到同一目录。 - 选中
.json文件,运行「动作构建」即可生成并安装动作。
浙公网安备 33010602011771号