Freigeben über


Список дел на рабочем столе

Опубликовано 4 августа 2009 в 09:47:00 | Coding4Fun

clip_image002

Если у вас всегда остается куча недоделанных дел, о которых надо напоминать, эта программа для вас. Это приложение, которое познакомит вас с WPF на примере реализации списка дел, расположенного на рабочем столе. В нем применяются такие вещи, как pinvoke, «прозрачные» приложения, использование компонентов Win32 наряду с WPF, а также техника обнаружения модификации файла.

Клинт Раткэс ( Clint Rutkas)
https://betterthaneveryone.com

Почему WPF-приложение?

Windows Presentation Foundation — исключительно мощный набор инструментов, позволяющий создавать полнофункциональные приложения с минимальными усилиями. Я перешел на WPF-приложения, отказавшись от Windows Form, из-за возможности применения эффектов прозрачности и свечения. Да и других полезных компонентов предостаточно.

Связывание приложения с XAML

XAML во многом похож на HTML или XML. С его помощью вы описываете, как должна выглядеть ваша программа. Я привожу очень простой пример, если вам нужны подробности, обращайтесь ко мне.

    1: <Window x:Class="ToDo_CSharp.MainWindow"
    2:     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3:     xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    4:     Title="ToDo" Height="200"  Width="300" Left="0"
    5:         ShowInTaskbar="False" BorderBrush="Transparent" Icon="report.ico" 
    6:         Background="Transparent" ResizeMode="NoResize" 
    7:         BorderThickness="0" WindowStyle="None" AllowsTransparency="True"
    8:         Loaded="Window_Loaded" StateChanged="Window_StateChanged" IsVisibleChanged="Window_IsVisibleChanged">
    9:         
   10:     <WrapPanel Name="wpTodo" Orientation="Vertical"></WrapPanel>
   11: </Window>

Привязка к событиям в WPF

Нам надо обрабатывать некоторые события: Loaded (окно загружено) , StateChanged (изменилось состояние) и IsVisibleChanged (изменилась ли видимость) .

При загрузке делаем следующее:

C#

    1: private void Window_Loaded(object sender, RoutedEventArgs e)
    2: {
    3:     SetWindowLocation();
    4:     Win32.HideFromAltTab(handle);
    5:     
    6:     Width = SystemParameters.FullPrimaryScreenWidth;
    7:     Top = SystemParameters.PrimaryScreenHeight - Height - 10;
    8:  
    9:     CreateNotifyIcon();
   10:     VerifyToDoFileLocation();
   11:  
   12:     if (File.Exists(TodoFileFullPath))
   13:         fillWrapPanel();
   14:  
   15:     createFileSystemWatcher();
   16: }

VB

    1: Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
    2:     SetWindowLocation()
    3:     Win32.HideFromAltTab(Handle)
    4:  
    5:     Width = SystemParameters.FullPrimaryScreenWidth
    6:     Top = SystemParameters.FullPrimaryScreenHeight - Height + 22
    7:  
    8:     CreateNotifyIcon()
    9:     VerifyToDoFileLocation()
   10:  
   11:     If File.Exists(TodoFileFullPath) Then
   12:         fillWrapPanel()
   13:     End If
   14:  
   15:     createFileSystemWatcher()
   16: End Sub

Состояние меняется, когда конечный пользователь делает что-то типа минимизации или максимизации окна. Если приложение минимизируется, мы принудительно возвращаем его в видимое состояние.

C#

    1: if (WindowState == WindowState.Minimized)
    2:     WindowState = WindowState.Normal;

VB

    1: If WindowState = WindowState.Minimized Then
    2:     WindowState = WindowState.Normal
    3: End If

Пора вернуться к Windows Forms

Когда я писал это приложение, в WPF не было элемента управления для отображения значков в панели задач, известного также как NotifyIcon. Может быть он уже появился, но мне в свое время пришлось применять компонент Windows Form NotifyIcon. Поскольку у нас .Net-приложение, доступ к этому компоненту достаточно прост, но все его свойства надо устанавливать вручную. Нам также придется вручную создавать связанное с этим компонентом контекстное меню. Надо будет добавить ссылку на System . Windows . Forms, но мы переименуем ее в forms при вызове, чтобы избежать конфликта пространств имен. Нам придется это сделать, поскольку вызываемые элементы имеют те же имена в других пространствах имен. В дальнейшем эта часть приложения будет заменена — будет применяться инфраструктура запуска утилит MEF (EN).

C#

    1: private void CreateNotifyIcon()
    2: {
    3:     _notifyIcon = new forms.NotifyIcon { Text = "ToDo", Icon = new Icon("report.ico"), Visible = true };
    4:  
    5:     var contextMenu = new forms.ContextMenuStrip { Name = "contextMenu" };
    6:     var exitToolStripMenuItem = new forms.ToolStripMenuItem { Name = "exitToolStripMenuItem1", Text = "Exit" };
    7:     var configurationMenuItem = new forms.ToolStripMenuItem { Name = "configurationMenuItem", Text = "Options" };
    8:     var openMenuItem = new forms.ToolStripMenuItem { Name = "openMenuItem", Text = "Open ToDo" };
    9:  
   10:     // contextMenu
   11:     contextMenu.Items.AddRange(new forms.ToolStripItem[] { openMenuItem, configurationMenuItem, exitToolStripMenuItem });
   12:     contextMenu.Size = new Size(155, 114);
   13:  
   14:     exitToolStripMenuItem.Click += exitToolStripMenuItem_Click;
   15:     configurationMenuItem.Click += configurationMenuItem_Click;
   16:     openMenuItem.Click += openMenuItem_Click;
   17:     _notifyIcon.ContextMenuStrip = contextMenu;
   18: }

VB

    1: Private Sub CreateNotifyIcon()
    2:     _notifyIcon = New _forms.NotifyIcon With {.Text = "ToDo", .Icon = New Icon("report.ico"), .Visible = True}
    3:  
    4:     Dim contextMenu = New Forms.ContextMenuStrip With {.Name = "contextMenu"}
    5:     Dim exitToolStripMenuItem = New Forms.ToolStripMenuItem With {.Name = "exitToolStripMenuItem1", .Text = "Exit"}
    6:     Dim configurationMenuItem = New Forms.ToolStripMenuItem With {.Name = "configurationMenuItem", .Text = "Options"}
    7:     Dim openMenuItem = New Forms.ToolStripMenuItem With {.Name = "openMenuItem", .Text = "Open ToDo"}
    8:  
    9:     ' contextMenu
   10:     contextMenu.Items.AddRange(New Forms.ToolStripItem() {openMenuItem, configurationMenuItem, exitToolStripMenuItem})
   11:     contextMenu.Size = New Size(155, 114)
   12:  
   13:     AddHandler exitToolStripMenuItem.Click, AddressOf exitToolStripMenuItem_Click
   14:     AddHandler configurationMenuItem.Click, AddressOf configurationMenuItem_Click
   15:     AddHandler openMenuItem.Click, AddressOf openMenuItem_Click
   16:     _notifyIcon.ContextMenuStrip = contextMenu
   17: End Sub

Использование PInvoke

Хотя .Net предлагает массу полезных возможностей, остаются вещи, которые лучше реализовывать через «родные» API. Отличные вызовы pinvoke можно найти на www.pinvoke.net (EN). Нам pinvoke потребуется для того, чтобы «спрятаться» от ALT-Tab, и для принудительного возврата приложения на рабочий стол. Я написал класс, реализующий эти функции без особых ухищрений. Все вызовы pinvoke у меня содержатся в частных методах.

Вот как реализована защита от ALT-Tab:

C#

    1: [DllImport("user32.dll")]
    2: private static extern int SetWindowLong(IntPtr window, int index, int value);
    3:  
    4: [DllImport("user32.dll")]
    5: private static extern int GetWindowLong(IntPtr window, int index);
    6:  
    7: // код для alt tab с сайта https://bytes.com/forum/thread442047.html
    8: private const int GWL_EXSTYLE = -20;
    9: private const int WS_EX_TOOLWINDOW = 0x00000080;
   10:  
   11: public static void HideFromAltTab(IntPtr Handle)
   12: {
   13:     SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) | WS_EX_TOOLWINDOW);
   14: }

VB

    1: <DllImport("user32.dll")> _
    2: Private Shared Function SetWindowLong(ByVal window As IntPtr, ByVal index As Integer, ByVal value As Integer) As Integer
    3: End Function
    4:  
    5: <DllImport("user32.dll")> _
    6: Private Shared Function GetWindowLong(ByVal window As IntPtr, ByVal index As Integer) As Integer
    7: End Function
    8:  
    9: ' код для alt tab с сайта https://bytes.com/forum/thread442047.html
   10: Private Const GWL_EXSTYLE As Integer = -20
   11: Private Const WS_EX_TOOLWINDOW As Integer = &H80
   12:  
   13: Public Shared Sub HideFromAltTab(ByVal Handle As IntPtr)
   14:     SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) Or WS_EX_TOOLWINDOW)
   15: End Sub
   16:  
   17: Public Shared Sub SetProcessWorkingSetSize()
   18:     SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle, -1, -1)
   19: End Sub

Установка положения окна требует чуть больше усилий:

C#

    1: // флаги положения окна
    2: static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
    3: static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
    4: static readonly IntPtr HWND_TOP = new IntPtr(0);
    5: static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
    6:  
    7: public const uint SWP_NOMOVE = 0x2;
    8: public const uint SWP_NOSIZE = 0x1;
    9: public const uint SWP_SHOWWINDOW = 0x40;
   10:  
   11: // https://www.developer.com/net/csharp/article.php/3347251
   12: // и https://msdn.microsoft.com/en-us/library/ms633545(VS.85).aspx
   13: // и https://pinvoke.net/default.aspx/user32.SetWindowPos
   14: [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
   15: public static extern bool SetWindowPos(
   16:     IntPtr hWnd, // window handle
   17:     IntPtr hWndInsertAfter, // placement-order handle
   18:     int X, // horizontal position
   19:     int Y, // vertical position
   20:     int cx, // width
   21:     int cy, // height
   22:     uint uFlags);
   23:  
   24: public static void SetWindowInBack(IntPtr handle)
   25: {
   26:     SetWindowPos(handle,
   27:        HWND_NOTOPMOST,
   28:        0, 0, 0, 0,
   29:        SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
   30:  
   31:     SetWindowPos(handle,
   32:        HWND_BOTTOM,
   33:        0, 0, 0, 0,
   34:        SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
   35: }
   36:  
   37: public static void SetWindowInFront(IntPtr handle)
   38: {
   39:     SetWindowPos(handle,
   40:        HWND_TOPMOST,
   41:        0, 0, 0, 0,
   42:        SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
   43: }

VB

    1: ' флаги положения окна
    2: Shared ReadOnly HWND_TOPMOST As New IntPtr(-1)
    3: Shared ReadOnly HWND_NOTOPMOST As New IntPtr(-2)
    4: Shared ReadOnly HWND_TOP As New IntPtr(0)
    5: Shared ReadOnly HWND_BOTTOM As New IntPtr(1)
    6:     
    7: Public Const SWP_NOMOVE As UInteger = &H2
    8: Public Const SWP_NOSIZE As UInteger = &H1
    9: Public Const SWP_SHOWWINDOW As UInteger = &H40
   10:  
   11: ' https://www.developer.com/net/csharp/article.php/3347251
   12: ' и https://msdn.microsoft.com/en-us/library/ms633545(VS.85).aspx
   13: ' и https://pinvoke.net/default.aspx/user32.SetWindowPos
   14:  
   15: <DllImport("user32.dll", EntryPoint:="SetWindowPos")> _
   16: Public Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As IntPtr, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, _
   17:  ByVal uFlags As UInteger) As Boolean
   18: End Function
   19:  
   20: Public Shared Sub SetWindowInBack(ByVal Handle As IntPtr)
   21:     SetWindowPos(Handle, HWND_NOTOPMOST, 0, 0, 0, 0, _
   22:      SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
   23:  
   24:     SetWindowPos(Handle, HWND_BOTTOM, 0, 0, 0, 0, _
   25:      SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
   26: End Sub
   27:  
   28: Public Shared Sub SetWindowInFront(ByVal Handle As IntPtr)
   29:     SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, _
   30:      SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
   31: End Sub

Наблюдение за файлами

В .Net есть замечательный класс — FileSystemWatcher. Он наблюдает за папкой или файлом и сообщает, когда происходит нечто интересное. В нашем случае надо отслеживать создание или изменение файла. Если кто-то изменяет интересующий нас файл, добавляя в него текст или удаляя из него данные, генерируется событие. Поскольку событие возникает в потоке, не использующем интерфейс пользователя, нам надо уведомить объект Dispatcher формы о необходимости обновления панели, посредством функции fillWrapPanel.

C#

    1: private void createFileSystemWatcher()
    2: {
    3:     todoWatcher = new FileSystemWatcher { Path = Settings.Default.ToDoFilePath, Filter = Settings.Default.ToDoFileName };
    4:  
    5:     todoWatcher.Changed += todoWatcher_Changed;
    6:     todoWatcher.Created += todoWatcher_Changed;
    7:  
    8:     todoWatcher.EnableRaisingEvents = true;
    9: }
   10:  
   11: private void todoWatcher_Changed(object sender, FileSystemEventArgs e)
   12: {
   13:     wpTodo.Dispatcher.Invoke(new NoArgDelegate(fillWrapPanel));
   14: }

VB

    1: Private Sub createFileSystemWatcher()
    2:     todoWatcher = New FileSystemWatcher With {.Path = My.Settings.ToDoFilePath, .Filter = My.Settings.ToDoFileName}
    3:  
    4:     AddHandler TodoWatcher.Changed, AddressOf todoWatcher_Changed
    5:     AddHandler TodoWatcher.Created, AddressOf todoWatcher_Changed
    6:  
    7:     TodoWatcher.EnableRaisingEvents = True
    8: End Sub
    9:  
   10: Private Sub todoWatcher_Changed(ByVal sender As Object, ByVal e As FileSystemEventArgs)
   11:     wpTodo.Dispatcher.Invoke(New NoArgDelegate(AddressOf fillWrapPanel))
   12: End Sub

Заполнение панели светящимися объектами

Обновление панели не представляет особого труда. Вы создаете объект Label и добавляете его к форме. Файл со списком дел разбивается на строки (по символу разрыва строки) и каждая из этих строк помещается в Label. Чтобы текст было проще читать, мы также применим эффект OuterGlowBitmapEffect.

C#

    1: private void fillWrapPanel()
    2: {
    3:     // файл существует и не используется?
    4:     string todoFileText = string.Empty;
    5:  
    6:     if (File.Exists(TodoFileFullPath))
    7:     {
    8:         while (FileInUse(TodoFileFullPath))
    9:             Thread.Sleep(50);
   10:         
   11:         todoFileText = File.ReadAllText(TodoFileFullPath);
   12:     }
   13:  
   14:     wpTodo.Children.Clear();
   15:     string[] items = Regex.Split(todoFileText, Environment.NewLine);
   16:  
   17:     foreach (string item in items)
   18:         wpTodo.Children.Add(CreateLabel(item));
   19: }
   20:  
   21: private Label CreateLabel(string item)
   22: {
   23:     var txt = new Label
   24:     {
   25:         Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(0, 0, 0, 0)),
   26:         Foreground = new SolidColorBrush(Settings.Default.FontForeColor),
   27:         AllowDrop = false,
   28:  
   29:         Focusable = false,
   30:         BorderThickness = new Thickness(0),
   31:         Content = item
   32:     };
   33:     txt.MouseDown += txt_MouseDown;
   34:  
   35:     if (Settings.Default.FontGlowColor.A != 0)
   36:         txt.BitmapEffect = new OuterGlowBitmapEffect
   37:         {
   38:             GlowColor = Settings.Default.FontGlowColor,
   39:             GlowSize = 7
   40:         };
   41:  
   42:     return txt;
   43: }
   44:  
   45: void txt_MouseDown(object sender, MouseButtonEventArgs e)
   46: {
   47:     SetWindowLocation();
   48: }

VB

    1: Private Sub fillWrapPanel()
    2:     ' файл существует и не используется?
    3:     Dim todoFileText As String = String.Empty
    4:  
    5:     If File.Exists(TodoFileFullPath) Then
    6:         While FileInUse(TodoFileFullPath)
    7:             Thread.Sleep(50)
    8:         End While
    9:  
   10:         todoFileText = File.ReadAllText(TodoFileFullPath)
   11:     End If
   12:  
   13:     wpTodo.Children.Clear()
   14:     Dim items As String() = Regex.Split(todoFileText, Environment.NewLine)
   15:  
   16:     For Each item As String In items
   17:         wpTodo.Children.Add(CreateLabel(item))
   18:     Next
   19: End Sub
   20:  
   21: Private Function CreateLabel(ByVal item As String) As Label
   22:  
   23:     Dim txt As New Label() With { _
   24:       .Background = New SolidColorBrush(System.Windows.Media.Color.FromArgb(0, 0, 0, 0)), _
   25:       .Foreground = New SolidColorBrush(My.Settings.FontForeColor), .AllowDrop = False, _
   26:       .Focusable = False, .BorderThickness = New Thickness(0), .Content = item}
   27:  
   28:     AddHandler txt.MouseDown, AddressOf txt_MouseDown
   29:  
   30:     If My.Settings.FontGlowColor.A <> 0 Then
   31:         txt.BitmapEffect = New OuterGlowBitmapEffect() With {.GlowColor = My.Settings.FontGlowColor, .GlowSize = 7}
   32:     End If
   33:  
   34:     Return txt
   35: End Function
   36:  
   37: Sub txt_MouseDown(ByVal sender As Object, ByVal e As MouseButtonEventArgs)
   38:     SetWindowLocation()
   39: End Sub

Завершение

Это приложение немного упорядочило мою жизнь и позволило ознакомиться с WPF. Если вы хотите больше внимания уделить внешнему виду WPF-приложения, советую вам использовать не Visual Studio, а Expression Blend .

Ссылка на исходный код игры приведена в начале статьи — загружайте и пробуйте!

Об авторе

Клинт Раткэс — популяризатор технологий Microsoft в учебных заведениях. Два его основные языка разработки — C# и JavaScript. Он занимается самыми разнообразными делами: например, он соорудил управляемый компьютером танцпол (EN), автоматизированного бармена и скейтборд с использованием C#. О том, как создавались эти штуки, а главное — зачем, рассказывается в его блоге: https://www.betterthaneveryone.com (EN). Можете также следить за ним в твиттере (@ClintRutkas) или написать ему по электронной почте: clint.rutkas@microsoft.com.

Comments

  • Anonymous
    June 07, 2011
    При сохранении файла todo.txt при открытой программе, она вылетает с ошибкой. Хотелось больше настроек отображения и возможность перетаскивать список по рабочему столу. Русские буквы отображает, если сохранить todo.txt в юникоде.