Пишем виртуальный буфер обмена на C#

VirtualClipBoard Очень много приходится работать с текстовыми данными, такими как код, статьи, посты и т.д. В то время когда жил под Linux — пользовался менеджерами истории буфера обмена, которые запоминали, то что попадало в виде текста в буфер и по клику в трее я мог вернуть нужное значение в буфер, не возвращаясь к источнику.
Недавно пришлось большую часть времени проводить в Windows, удовлетворяющей альтернативы для такого простого приложения я не нашел. Что-то в найденных вариантах не устраивало: не свободное ПО, много ненужного функционала (который просто мешал) или работали неудобно для меня (например: получить предыдущее значение буфера, приходилось открывать окно программы). Недолго думая решил сделать, так как мне хотелось.

Поскольку, приложение должно работать исключительно в Windows, было принято решение написать ее на C# — к тому же, никогда ранее на нем ничего не писал — появился повод попробовать.

Задача


  • Программа должна слушать и запоминать изменения в текстовом буфере обмена.
  • Через контекстное меню в трее или через окно программы по клику на выбранном элементе истории, нужное значение должно автоматически попадать назад в буфер.
  • История не должна пропадать после перезагрузки системы




Интерфейс


  • Возможность поставить приложение на автозагрузку в реестр.
  • Изменить количество хранимой истории.
  • Изменить количество последних элементов буфера выводимых через контекстное меню в трее.
  • Принудительная очистка истории.
  • Выход из программы, поскольку при закрытии программа должна сворачиваться в трей.


VirtualClipBoard

Выкладывать буду основные части кода с небольшими пояснениями. В конце под спойлер выложен полный листинг программы, а так же ссылка на полный проект упакованный в zip архив + отдельно ссылка на скомпилированную версию программы в виде exe файла.


Непосредственно сам код



Изобретать велосипед не хотелось для хранения настроек программы, поэтому «размера истории» и «количество выводимых элементов в трее» — использовал Properties.Settings.Default:

        // изменение размера истории
        private void history_size_ValueChanged(object sender, EventArgs e)
        {
            Properties.Settings.Default.history_size = (int)history_size.Value;
            Properties.Settings.Default.Save();
            Console.WriteLine("Размер истории изменен: " + Properties.Settings.Default.history_size);
            reload_list_clipboard(); // Обновляем ListBox
        }

        // изменение количества записей БО в трее
        private void size_tray_ValueChanged(object sender, EventArgs e)
        {
            Properties.Settings.Default.size_tray = (int)size_tray.Value;
            Properties.Settings.Default.Save();
            Console.WriteLine("Количество элементов в трее изменено: " + Properties.Settings.Default.size_tray);
            reload_tray(); // Обновляем Трей
        }


Автозагрузка, программы, как было оговорено ранее, реализована через реестр.
        // Событие изменения статуса флажка автозагрузки
        // Если флажок - прописываем в реестр на автозагрузку
        private void autoload_CheckedChanged(object sender, EventArgs e)
        {
            RegistryKey reg = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run\", true);
            if (reg.GetValue(VirtualClipBoard_Name) != null)
            {
                try
                {
                    reg.DeleteValue(VirtualClipBoard_Name);
                    Console.WriteLine("Программа " + VirtualClipBoard_Name + " удалена из автозагрузки в реестре");
                }
                catch
                {
                    Console.WriteLine("Ошибка удаления " + VirtualClipBoard_Name + " из автозагрузки в реестре");
                }
            }
            if(autoload.Checked)
            {
                reg.SetValue(VirtualClipBoard_Name, Application.ExecutablePath);
                Console.WriteLine("Программа " + VirtualClipBoard_Name + " записана в автозагрузку через реестр");
            }
            reg.Close();
        }


При закрытии окна, программа должна сворачиваться в трей, а не завершать работу
        // Сворачивать в трей вместо закрытия программы
        protected override void OnClosing(CancelEventArgs e)
        {
            e.Cancel = true;
            // ShowInTaskbar = false;
            Hide();
        }


Метод для завершения работы программы
        // Завершение работы программы по закрытию через кнопку
        private void exit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }


Поскольку, проверять по таймеру изменения в буфере обмена — это извращение + неоправданная трата ресурсов, мы будем использовать User32.dll. Для этого нам необходимо добавить наше окно к цепочки окон буфера обмена и используя WndProc принимать сообщения, а точнее WM_DRAWCLIPBOARD = 0x0308, которое уведомит нас об изменении в буфере обмена.

Итак, 1. Подключаем библиотеки:
        [DllImport("User32.dll")]
        protected static extern int SetClipboardViewer(int hWndNewViewer);
        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);


2. Реализация метода WndProc принимающего сообщения и отправляющее их далее по цепочке:
        // дескриптор окна
        private IntPtr nextClipboardViewer;

        // Константы
        public const int WM_DRAWCLIPBOARD = 0x0308;
        public const int WM_CHANGECBCHAIN = 0x030D;

        // Метод для реагирование на изменение вбуфере обмена и т.д.
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_DRAWCLIPBOARD:
                    {
                        ClipboardChanged();
                        SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
                        break;
                    }
                case WM_CHANGECBCHAIN:
                    {
                        if (m.WParam == nextClipboardViewer)
                        {
                            nextClipboardViewer = m.LParam;
                        }
                        else
                        {
                            SendMessage(nextClipboardViewer, WM_CHANGECBCHAIN, m.WParam, m.LParam);
                        }
                        m.Result = IntPtr.Zero;
                        break;
                    }
                default:
                    {
                        base.WndProc(ref m);
                        break;
                    }
            }
        }


3. В методе загрузки формы зарегистрируем наше окно:
nextClipboardViewer = (IntPtr)SetClipboardViewer((int)this.Handle);


4. Теперь необходимо реагировать на изменение в буфере и записывать их в историю, для этого создадим метод ClipboardChanged. Запись истории будет происходить в словарь Dictionary<int, string> VirtualClipBoard_History = new Dictionary<int, string>();, если количество элементов в словаре больше, чем размер истории, в методе также введем очистку от более старых элементов. Так же, для возможности получить историю предыдущей сессии, будем записывать новые элементы в файл VirtualClipBoard_DAT.

        // Реагируем на обновление буфераобмена
        private void ClipboardChanged()
        {
            if (Clipboard.ContainsText() && Clipboard.GetText().Length > 0 && VirtualClipBoard_TARGET != Clipboard.GetText())
            {
                VirtualClipBoard_TARGET = Clipboard.GetText();

                // Записываем новый элемент в словарь
                VirtualClipBoard_History.Add((VirtualClipBoard_History.Last().Key + 1), VirtualClipBoard_TARGET);

                reload_tray(); // Обноавляемменю в трее
                reload_list_clipboard(); // Обновляем ListBox

                // Отчистка словаря от лишних элементов
                if (VirtualClipBoard_History.Count() > Properties.Settings.Default.history_size)
                {
                    int clear_items_count = VirtualClipBoard_History.Count() - Properties.Settings.Default.history_size;
                    var list = VirtualClipBoard_History.Keys.ToList();
                    list.Sort();
                    foreach (var key in list)
                    {
                        VirtualClipBoard_History.Remove(key);
                        if (clear_items_count == 1) { break; } else { clear_items_count--; }
                    }
                }

                // Записываем новый элемент в файл истории
                StreamWriter writer = new StreamWriter(VirtualClipBoard_DAT, true, System.Text.Encoding.UTF8);
                writer.WriteLine(@"<item>" + VirtualClipBoard_TARGET.Replace(@"<", @"<").Replace(@">", @">") + @"</item>");
                writer.Close();
                Console.WriteLine("В историю добавлен новый элемент: " + VirtualClipBoard_TARGET);
            }
        }


Для вывода элементов в трей создадим метод reload_tray. Использовать будем ContextMenuStrip. Для каждого ToolStripMenuItem будем использовать TAG, в который будем передавать ключ в словаре, чтоб при выборе элемента в контекстном меню легко вытащить нужный элемент из словаря.
        // Перезагрузка элементов для трей
        private void reload_tray()
        {
            ContextMenuStrip contextMenu = new ContextMenuStrip();
            ToolStripMenuItem menuItem;

            int free_slot_to_tray = Properties.Settings.Default.size_tray;
            var list = VirtualClipBoard_History.OrderByDescending(x => x.Key);
            foreach (var item in list)
            {
                menuItem = new ToolStripMenuItem();
                menuItem.Tag = item.Key;
                if (item.Value.Length > 60)
                {
                    menuItem.Text = item.Value.Replace("\n", "\t").Replace("\r", "\t").Substring(0, 60);
                } else {
                    menuItem.Text = item.Value.Replace("\n", "\t").Replace("\r", "\t");
                }
                
                menuItem.Click += new System.EventHandler(menu_item_click);
                contextMenu.Items.Add(menuItem);
                if (free_slot_to_tray == 1) { break; } else { free_slot_to_tray--; }
            }

            // Разделитель
            contextMenu.Items.Add(new ToolStripSeparator());

            // Свернуть/Развернуть
            menuItem = new ToolStripMenuItem();
            menuItem.Text = "Настройки";
            menuItem.Click += new System.EventHandler(menu_item_config);
            contextMenu.Items.Add(menuItem);

            // Выход из программы
            menuItem = new ToolStripMenuItem();
            menuItem.Text = "Выход";
            menuItem.Click += new System.EventHandler(exit_Click);
            contextMenu.Items.Add(menuItem);

            _notifyIcon.ContextMenuStrip = contextMenu;
        }


События обработки клика в контекстном меню в трее:
        // Событие по клику на элемент контекстного меню в трее
        private void menu_item_click(object sender, EventArgs e)
        {
            Clipboard.SetText(VirtualClipBoard_History[(int)(sender as ToolStripMenuItem).Tag]);
        }


Генерация списка для ListBox в самой форме программы. Для удобства будем использовать дополнительный словарь VirtualClipBoard_Index_ListBox = new Dictionary<int, int>(); чтоб связать элемент ListBox c элементом в словаре истории VirtualClipBoard_History:
        // Перезагрузка элементов в ListBox
        private void reload_list_clipboard()
        {
            VirtualClipBoard_Index_ListBox = new Dictionary<int, int>();
            int list_target_item = 0; // индекс текущего элемента в ListBox
            list_clipboard.Items.Clear(); // Очищаем список
            String string_name_ite;
            int free_slot_to_tray = Properties.Settings.Default.history_size;
            var list = VirtualClipBoard_History.OrderByDescending(x => x.Key);
            foreach (var item in list)
            {
                if (item.Value.Length > 150)
                {
                    string_name_ite = item.Value.Replace("\n", "\t").Replace("\r", "\t").Substring(0, 60);
                }
                else
                {
                    string_name_ite = item.Value.Replace("\n", "\t").Replace("\r", "\t");
                }
                list_clipboard.Items.Add(string_name_ite);
                VirtualClipBoard_Index_ListBox.Add(list_target_item, item.Key);
                if (free_slot_to_tray == 1) { break; } else { free_slot_to_tray--; }
                list_target_item++; // Увеличиваем индекс текущего элемента в ListBox
            }
        }


При выборе элемента в ListBox — событие list_clipboard_SelectedIndexChanged:
        // Выбор элемента в ListBox
        private void list_clipboard_SelectedIndexChanged(object sender, EventArgs e)
        {
            Clipboard.SetText(VirtualClipBoard_History[VirtualClipBoard_Index_ListBox[list_clipboard.SelectedIndex]]);
        }


Во время загрузки программы, считываем историю из файла VirtualClipBoard_DAT и парсим данные, как XML.
Элементы истории буфера обмена хранить отдельно в каждой строчке в нашем случае не получится, поскольку элементы могут иметь символы перевода каретки (\n), которые могут повредить нам целостность данных при считывании.
Если использовать serialize, то можно потерять на быстродействии программы при каждом изменении буфера.
Правильней конечно было бы сохранять историю в файл при закрытии программы, но мне этот вариант что-то не по душе пришел в этом случае, поэтому организовал именно таким способом (кого-то могу этим зацепить — сразу прошу прощения).
Кодировать данные так же не хотел, хотелось оставить исходный вид данных, чтоб при необходимости можно было читать файл истории через блокнот.
            // Загружаем историю из файла
            String XMLString = "";
            XMLString += @"<items>";
            if (File.Exists(VirtualClipBoard_DAT))
            {
                StreamReader stream = new StreamReader(VirtualClipBoard_DAT);
                while (stream.Peek() > -1)
                {
                    XMLString += stream.ReadLine() + "\n";
                }
                stream.Close();
                XMLString += @"</items>";
                int index_new_history = 2;
                XDocument doc = XDocument.Parse(XMLString);
                var items = doc.Element("items").Elements("item");
                foreach (XElement item in items)
                {
                    VirtualClipBoard_History.Add(index_new_history, item.Value);
                    index_new_history++; // увеличиваем индекс новому элементу
                }
            }


Весь код проекта


Весь код проекта
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Data.Sql;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Windows;

namespace VirtualClipBoard
{
    public partial class VirtualClipBoard : Form
    {
        String VirtualClipBoard_Name = "VirtualClipBoard"; // название программы
        public String VirtualClipBoard_TARGET; // последний значение текстового БО
        public String VirtualClipBoard_DAT; // путь к файлу истории
        Dictionary<int, string> VirtualClipBoard_History = new Dictionary<int, string>(); // История нашего буфера
        Dictionary<int, int> VirtualClipBoard_Index_ListBox; // список индексов в связки с ключами истории буфера

        // Подключение библиотек WIN
        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);
        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);

        // Наш Form
        public VirtualClipBoard()
        {
            InitializeComponent();
            load_configs();

            nextClipboardViewer = (IntPtr)SetClipboardViewer((IntPtr)this.Handle);

            reload_tray(); // Обноавляемменю в трее
            reload_list_clipboard(); // Обновляем ListBox

            _notifyIcon.Text = VirtualClipBoard_Name;
            _notifyIcon.MouseDoubleClick += new MouseEventHandler(_notifyIcon_MouseDoubleClick);
        }

        // Перезагрузка элементов в ListBox
        private void reload_list_clipboard()
        {
            VirtualClipBoard_Index_ListBox = new Dictionary<int, int>();
            int list_target_item = 0; // индекс текущего элемента в ListBox
            list_clipboard.Items.Clear(); // Очищаем список
            String string_name_ite;
            int free_slot_to_tray = Properties.Settings.Default.history_size;
            var list = VirtualClipBoard_History.OrderByDescending(x => x.Key);
            foreach (var item in list)
            {
                if (item.Value.Length > 150)
                {
                    string_name_ite = item.Value.Replace("\n", "\t").Replace("\r", "\t").Substring(0, 60);
                }
                else
                {
                    string_name_ite = item.Value.Replace("\n", "\t").Replace("\r", "\t");
                }
                list_clipboard.Items.Add(string_name_ite);
                VirtualClipBoard_Index_ListBox.Add(list_target_item, item.Key);
                if (free_slot_to_tray == 1) { break; } else { free_slot_to_tray--; }
                list_target_item++; // Увеличиваем индекс текущего элемента в ListBox
            }
        }

        // Выбор элемента в ListBox
        private void list_clipboard_SelectedIndexChanged(object sender, EventArgs e)
        {
            Clipboard.SetText(VirtualClipBoard_History[VirtualClipBoard_Index_ListBox[list_clipboard.SelectedIndex]]);
        }

        // Перезагрузка элементов для трей
        private void reload_tray()
        {
            ContextMenuStrip contextMenu = new ContextMenuStrip();
            ToolStripMenuItem menuItem;

            int free_slot_to_tray = Properties.Settings.Default.size_tray;
            var list = VirtualClipBoard_History.OrderByDescending(x => x.Key);
            foreach (var item in list)
            {
                menuItem = new ToolStripMenuItem();
                menuItem.Tag = item.Key;
                if (item.Value.Length > 60)
                {
                    menuItem.Text = item.Value.Replace("\n", "\t").Replace("\r", "\t").Substring(0, 60);
                } else {
                    menuItem.Text = item.Value.Replace("\n", "\t").Replace("\r", "\t");
                }
                
                menuItem.Click += new System.EventHandler(menu_item_click);
                contextMenu.Items.Add(menuItem);
                if (free_slot_to_tray == 1) { break; } else { free_slot_to_tray--; }
            }

            // Разделитель
            contextMenu.Items.Add(new ToolStripSeparator());

            // Свернуть/Развернуть
            menuItem = new ToolStripMenuItem();
            menuItem.Text = "Настройки";
            menuItem.Click += new System.EventHandler(menu_item_config);
            contextMenu.Items.Add(menuItem);

            // Выход из программы
            menuItem = new ToolStripMenuItem();
            menuItem.Text = "Выход";
            menuItem.Click += new System.EventHandler(exit_Click);
            contextMenu.Items.Add(menuItem);

            _notifyIcon.ContextMenuStrip = contextMenu;
        }

        // Вызов окна настроек
        private void menu_item_config(object sender, EventArgs e)
        {
            // ShowInTaskbar = true;
            Show();
            WindowState = FormWindowState.Normal;
        }

        // Событие по клику на элемент контекстного меню в трее
        private void menu_item_click(object sender, EventArgs e)
        {
            // Console.WriteLine((int)(sender as ToolStripMenuItem).Tag);
            Clipboard.SetText(VirtualClipBoard_History[(int)(sender as ToolStripMenuItem).Tag]);
        }

        // событие при клике мышкой по значку в трее
        private void _notifyIcon_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            Console.WriteLine(WindowState);
            if (WindowState == FormWindowState.Normal || WindowState == FormWindowState.Maximized)
            {
                // ShowInTaskbar = false;
                Hide();
                WindowState = FormWindowState.Minimized;
            }
            else
            {
                // ShowInTaskbar = true;
                Show();
                WindowState = FormWindowState.Normal;
            }
        }

        // Установка путей к файлам конфигурации и истории
        private void load_configs()
        {
            VirtualClipBoard_DAT = Application.UserAppDataPath + "\\history.dat";
            Console.WriteLine("Файл истории: " + VirtualClipBoard_DAT);
            history_size.Value = Properties.Settings.Default.history_size;
            Console.WriteLine("Размер истории загружен из настроек: " + Properties.Settings.Default.history_size);
            size_tray.Value = Properties.Settings.Default.size_tray;
            Console.WriteLine("Количество элементов в трее загружено из настроек: " + Properties.Settings.Default.size_tray);
            RegistryKey reg = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run\", true);
            if (reg.GetValue(VirtualClipBoard_Name) != null){
                autoload.Checked = true;
                Console.WriteLine("Приложение записано в автозагрузку. (В настройках ставим Checked = true)");
            }
            reg.Close();

            // Загружаем историю из файла
            String XMLString = "";
            XMLString += @"<items>";
            if (File.Exists(VirtualClipBoard_DAT))
            {
                StreamReader stream = new StreamReader(VirtualClipBoard_DAT);
                while (stream.Peek() > -1)
                {
                    XMLString += stream.ReadLine() + "\n";
                }
                stream.Close();
                XMLString += @"</items>";
                int index_new_history = 2;
                XDocument doc = XDocument.Parse(XMLString);
                var items = doc.Element("items").Elements("item");
                foreach (XElement item in items)
                {
                    VirtualClipBoard_History.Add(index_new_history, item.Value);
                    index_new_history++; // увеличиваем индекс новому элементу
                }
            }
            // Чистим историю буфера
            if (VirtualClipBoard_History.Count() > Properties.Settings.Default.history_size)
            {
                int clear_items_count = VirtualClipBoard_History.Count() - Properties.Settings.Default.history_size;
                var list = VirtualClipBoard_History.Keys.ToList();
                list.Sort();
                foreach (var key in list)
                {
                    VirtualClipBoard_History.Remove(key);
                    if (clear_items_count == 1) { break; } else { clear_items_count--; }
                }
            }
            // Обновляем файл истории
            StreamWriter writer = new StreamWriter(VirtualClipBoard_DAT, false, System.Text.Encoding.UTF8);
            var new_list = VirtualClipBoard_History.Keys.ToList();
            new_list.Sort();
            foreach (var key in new_list)
            {
                writer.WriteLine(@"<item>" + VirtualClipBoard_History[key].Replace(@"<", @"<").Replace(@">", @">") + @"</item>");
            }
            writer.Close();
            // Если элементов ноль, добавляем из буфера
            Console.WriteLine(VirtualClipBoard_History.Count());
            if (VirtualClipBoard_History.Count() == 0)
            {
                VirtualClipBoard_TARGET = Clipboard.GetText();
                VirtualClipBoard_History.Add(1, VirtualClipBoard_TARGET);
            }
            VirtualClipBoard_TARGET = VirtualClipBoard_History.Last().Value;
        }

        // Событие изменения статуса флажка автозагрузки
        // Если флажок - прописываем в реестр на автозагрузку
        private void autoload_CheckedChanged(object sender, EventArgs e)
        {
            RegistryKey reg = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run\", true);
            if (reg.GetValue(VirtualClipBoard_Name) != null)
            {
                try
                {
                    reg.DeleteValue(VirtualClipBoard_Name);
                    Console.WriteLine("Программа " + VirtualClipBoard_Name + " удалена из автозагрузки в реестре");
                }
                catch
                {
                    Console.WriteLine("Ошибка удаления " + VirtualClipBoard_Name + " из автозагрузки в реестре");
                }
            }
            if(autoload.Checked)
            {
                reg.SetValue(VirtualClipBoard_Name, Application.ExecutablePath);
                Console.WriteLine("Программа " + VirtualClipBoard_Name + " записана в автозагрузку через реестр");
            }
            reg.Close();
        }


        // Завершение работы программы по закрытию через кнопку
        private void exit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        // изменение размера истории
        private void history_size_ValueChanged(object sender, EventArgs e)
        {
            Properties.Settings.Default.history_size = (int)history_size.Value;
            Properties.Settings.Default.Save();
            Console.WriteLine("Размер истории изменен: " + Properties.Settings.Default.history_size);
            reload_list_clipboard(); // Обновляем ListBox
        }

        // изменение количества записей БО в трее
        private void size_tray_ValueChanged(object sender, EventArgs e)
        {
            Properties.Settings.Default.size_tray = (int)size_tray.Value;
            Properties.Settings.Default.Save();
            Console.WriteLine("Количество элементов в трее изменено: " + Properties.Settings.Default.size_tray);
            reload_tray(); // Обновляем Трей
        }

        // Реагируем на обновление буфераобмена
        private void ClipboardChanged()
        {
            if (Clipboard.ContainsText() && Clipboard.GetText().Length > 0 && VirtualClipBoard_TARGET != Clipboard.GetText())
            {
                VirtualClipBoard_TARGET = Clipboard.GetText();

                // Записываем новый элемент в словарь
                VirtualClipBoard_History.Add((VirtualClipBoard_History.Last().Key + 1), VirtualClipBoard_TARGET);

                reload_tray(); // Обноавляемменю в трее
                reload_list_clipboard(); // Обновляем ListBox

                // Отчистка словаря от лишних элементов
                if (VirtualClipBoard_History.Count() > Properties.Settings.Default.history_size)
                {
                    int clear_items_count = VirtualClipBoard_History.Count() - Properties.Settings.Default.history_size;
                    var list = VirtualClipBoard_History.Keys.ToList();
                    list.Sort();
                    foreach (var key in list)
                    {
                        VirtualClipBoard_History.Remove(key);
                        if (clear_items_count == 1) { break; } else { clear_items_count--; }
                    }
                }

                // Записываем новый элемент в файл истории
                StreamWriter writer = new StreamWriter(VirtualClipBoard_DAT, true, System.Text.Encoding.UTF8);
                writer.WriteLine(@"<item>" + VirtualClipBoard_TARGET.Replace(@"<", @"<").Replace(@">", @">") + @"</item>");
                writer.Close();
                Console.WriteLine("В историю добавлен новый элемент: " + VirtualClipBoard_TARGET);
            }
        }

        // Затираем всю историю
        private void clear_Click(object sender, EventArgs e)
        {
            StreamWriter writer = new StreamWriter(VirtualClipBoard_DAT, false, System.Text.Encoding.Default);
            writer.Write("");
            writer.Close();

            VirtualClipBoard_History = new Dictionary<int, string>();

            VirtualClipBoard_TARGET = Clipboard.GetText();
            VirtualClipBoard_History.Add(1, VirtualClipBoard_TARGET);

            reload_tray(); // Обноавляемменю в трее
            reload_list_clipboard(); // Обновляем ListBox
        }

        // Сворачивать в трей вместо закрытия программы
        protected override void OnClosing(CancelEventArgs e)
        {
            e.Cancel = true;
            //ShowInTaskbar = false;
            Hide();
            WindowState = FormWindowState.Minimized;
        }

        // дескриптор окна
        private IntPtr nextClipboardViewer;

        // Константы
        public const int WM_DRAWCLIPBOARD = 0x308;
        public const int WM_CHANGECBCHAIN = 0x030D;

        // Метод для реагирование на изменение вбуфере обмена и т.д.
        protected override void WndProc(ref Message m)
        {
            // Console.WriteLine("WndProc");
            switch (m.Msg)
            {
                case WM_DRAWCLIPBOARD:
                    {
                        ClipboardChanged();
                        Console.WriteLine("WM_DRAWCLIPBOARD ClipboardChanged();");
                        SendMessage(nextClipboardViewer, WM_DRAWCLIPBOARD, m.WParam, m.LParam);
                        break;
                    }
                case WM_CHANGECBCHAIN:
                    {
                        if (m.WParam == nextClipboardViewer)
                        {
                            nextClipboardViewer = m.LParam;
                        }
                        else
                        {
                            SendMessage(nextClipboardViewer, WM_CHANGECBCHAIN, m.WParam, m.LParam);
                        }
                        m.Result = IntPtr.Zero;
                        break;
                    }
                default:
                    {
                        base.WndProc(ref m);
                        break;
                    }
            }
        }
    }
}


Список литературы:


Properties.Settings.Default — сохранение пользовательских настроек приложения
Registry.LocalMachine — работа с реестром
SetClipboardViewer — добавляем определяемое окно к цепочке окон просмотра буфера обмена.
класс StreamWriter — запись в файл
класс ContextMenuStrip — контекстное меню
класс Dictionary — Представляет коллекцию ключей и значений.
класс Clipboard — Предоставляет методы для помещения данных в системный буфер обмена и извлечения данных из системного буфера обмена.

Ссылки на проект:


Скачать исходники всего проекта для VS в zip (133КБ)
Скачать скомпилированный EXE файл (225КБ)

UPDATE 1: Залил исходники на GitHub: https://github.com/yanzlatov/ClipBoard

UPDATE 2: Немного исправил код, поскольку обратил внимание на особенность, если установить ShowInTaskbar = false (Получает или задает значение, указывающее, отображается ли форма в панели задач Windows.) наше окно отваливается от цепочки прослушивания обновления буфера обмена и в последующем не получает обновление.

UPDATE 3: Заменено Microsoft.Win32.Registry.LocalMachine на Microsoft.Win32.Registry.CurrentUser. (Спасибо xaizek).

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 28

    +4
    Полезная тулза, спасибо!
    А код и релизные файлы удобнее хранить на гитхабе.
    +2
    Менеджер буфера обмена — это очень удобная вещь, когда привыкаешь им пользоваться. Я с давних времён пользовался функцией PuntoSwitcher «Следить за буфером обмена», но она периодически глючила. Потом использовал 3dclipboard, но там тоже появлялась та же ошибка, что в PuntoSwitcher. Сейчас пользуюсь clipdiary. Эта программа вроде работает довольно неплохо и удобна сама по себе.

    А почему бы не разместить исходники на каком-нибудь хостинге проектов, например, на Bitbucket или Github?
    Возможно, что ваша программа со временем станет лучше всех, да ещё и Open Source! :)
      +1
      clipdiary — ставил, мне ей не удобно пользоваться, поскольку для того, чтоб получить нужный элемент в истории, необходимо открывать окно программы, к тому же я не люблю разного рода лицензии (а эта программа не совсем свободна по условию использования, цитирую первую строку из условий: "Использование программы дома (за исключением случаев, когда домашний компьютер является рабочим — удаленная работа, фриланс )").

      На Github уже залил.
        +1
        … поскольку для того, чтоб получить нужный элемент в истории, необходимо открывать окно программы

        Не обязательно, есть глобальные горячие клавиши:
        Скрытый текст



        В общем, на вкус и цвет…
          0
          Это действительно на вкус и цвет, мне просто это не удобно. Возможно дело многолетней привычки в Linux, когда клипер всегда выводил в трее список истории — поэтому именно таки реализовал.
        +1
        Это не программа глючила, это вся организация цепочки ClipboardViewer в винде корявая.
        Вот увлекательное чтиво: blogs.msdn.com/b/rds/archive/2006/11/16/why-does-my-shared-clipboard-not-work-part-1.aspx
        +7
        Под windows могу порекомендовать ditto: ditto-cp.sourceforge.net Удобная программа.
          +2
          Все более менее известные перепробовал — ничего не удобно, самые хорошие менеджеры только под Linux — простые и ничего лишнего например клипер под KDE. В ditto — относительно много всего, но мне не нужны настройки, я хочу в контекстном меню в трее, сразу видеть меню из элементов истории, а не пункты по настройке программы.
          Для меня менеджер буфера обмена — это не более удобного блокнотика, который сам запишет все что я копирую и когда нужно я с него возьму.
            +1
            Тоже использую ditto. В настройки даже не заглядывал) Когда надо выбрать что-то из буфера, я нажимаю Ctrl + ` и стрелками выбираю. Никаких лишних движений.
            ЗЫ: надо бы сесть и что-то подобное под андроид написать
          +2
          Есть еще: тыц и тыдыц
          Вдруг кому пригодится.
            +3
            У меня тоже есть подобное творение. Правда, всё никак не соберусь довести до ума. Но раз уж пошла такая тема, оставлю ссылочку :)





            +4
            Немного вопрос не в тему, увидел использование Windows Forms и хотел спросить, вообще их ещё имеет смысл использовать, или всё таки надо WPF?
            PS интересно, кстати, какая позиция MS по этому поводу.
            PS2 программка полезная, поставил звездочку на гитхабе :)
              +1
              Мне кажется, здесь особо без разницы, что использовать) Тут внешнего дизайна, по сути, практически нет.
              Но насколько помню со времён моих ковырятельств в С#, работать с треем, юзая winforms, легче
                +2
                Сейчас используют WPF. Он же сейчас стандарт в Windows 8 и Windows Phone. Впрочем, никто не запрещает использовать Windows Forms, но он более не развивается и предоставляет меньше возможностей по построению интерфейса.
                +3
                Magistr_AVSH, я не специалист, просто любитель, но как по мне, не вижу смысла писать XAML там, где можно мышкой перетащить пару элементов формы и все будет работать, как задумано. Я бы выбирал то, что будет удобней в конкретной ситуации. Конечно интересные приложения, к тому же рассчитанные под тачпад, я бы лепил через WPF — возможностей больше и шаблоны облегчают жизнь.
                  +1
                  Ну подходы разные. Но в xaml по умолчанию писать никто не заставляет. Можно точно также перетаскивать элементы формы и расставлять их как удобно. А там немного потом коде поправить если нужно. События нажатий кнопок и т.д. Кому что больше нравится
                  +1
                  ClipX стоит давно, неплохая програмулина, хоть и старенькая.
                    +8
                    Microsoft.Win32.Registry.LocalMachine

                    Это требует прав администратора, что странно для такого рода приложения. Лучше использовать Registry.CurrentUser, тогда и предпочтения одного пользователя не будут навязываться всем остальным.
                      +3
                      Как-то не задумывался по этому поводу, но большое спасибо — поправлю.
                      +2
                      VirtualClipBoard_History[key].Replace(@"<", @"<").Replace(@">", @">")

                      Это разные кодировки? Или что это?
                      // Очистка словаря от лишних элементов

                      Как то сложно, для простого удаления n элементов с одного из концов списка…
                      // Чистим историю буфера

                      Зачем это делать, если load_configs только при запуске формы?
                      Вообще чем обусловлено использование Dictionary<int, string>, а не простого List?
                      За программку спасибо, давно уже пора было поставить подобный софт:)
                        +1
                        Это разные кодировки? Или что это?

                        Посмотрите строку из цитаты в исходниках, там html сущности скобок. Использование html сущностей данных кавычек избавляет нас от ошибок во время парсинга XML.

                        Вообще чем обусловлено использование Dictionary<int, string>, а не простого List?

                        Вообще можно написать и с помощью списков, но мне больше по душе именно словари, поскольку индекс элемента не изменяется.

                        Зачем это делать, если load_configs только при запуске формы?

                        load_configs — принципиально запускается только при старте программы и очистка файла истории происходит именно в этот, это по двум причинам: 1) Не хотелось тратить ресурсы и чистить после появления каждой новой записи, поскольку как правило моя история порядка 500 записей и бывают разные размеры элементов, 2) Если что-то случится, например погаснет свет или еще что-то, я всегда смогу восстановить всю историю предыдущей сессии (вряд ли понадобится, но в жизни всякое бывает).

                        P.S. Исходный код выложил специально, чтоб каждый желающий мог изменить его под себя, т.к. мелочи сугубо индивидуальная штука и каждому свое.
                          +1
                          работает ли она в modern-ui приложениях win8?
                            +1
                            Не проверял. Но теоретически, мы используем WinApi, а не слушаем каждое приложение, поэтому причин, чтоб это не работало не вижу.
                            +4
                            Ещё одна программа, в «копилку знаний» — CLCL www.nakka.com/soft/clcl/index_rus.html. Жаль, что не развивается. Но из массы протестированных, остановился на ней.
                            Вроде даже отвечает всем хотелкам автора :)
                              +1
                              Тоже когда-то делал аналог для Rainmeter — может пригодится кому:)
                                +1
                                Автор, при запуске программы и клике на пустом пространстве в поле истории, возникает такая ошибка:

                                See the end of this message for details on invoking 
                                just-in-time (JIT) debugging instead of this dialog box.
                                
                                ************** Exception Text **************
                                System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
                                   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
                                   at VirtualClipBoard.VirtualClipBoard.list_clipboard_SelectedIndexChanged(Object sender, EventArgs e)
                                   at System.Windows.Forms.ListBox.OnSelectedIndexChanged(EventArgs e)
                                   at System.Windows.Forms.ListBox.WmReflectCommand(Message& m)
                                   at System.Windows.Forms.ListBox.WndProc(Message& m)
                                   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
                                   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
                                   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
                                


                                А со второго раза вообще перестала запускаться :(
                                Надо бы пофиксить эти баги.
                                  +2
                                  Позже выложу обновленную версию

                                Only users with full accounts can post comments. Log in, please.