Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Todo: You stuff on your desktop

imageEver have 800 items to keep straight and always wished you're todo list was always in sight?  This application will show you a quick introduction to WPF and how to create a Desktop ToDo application.  It shows how to do some basic pinvokes, transparent applications, mixing WPF and Win32 controls, and how to know if a file has been modify.

A WPF application?  Why?

Windows Presentation Foundation is an extremely powerful set of tools that lets you create richer applications with minimal effort.  I went with WPF over Windows Form applications for the transparency and  glow effect I get for free.  I also have access to another control that did the layout for me too.

XAMLing up the application

XAML is just like HTML or XML for the most part.  You declare what you want and it renders how you want it to.  The declarations I used here are pretty straight forward, if you have any questions, please make a comment so I can answer them.

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

 

Wiring the WPF stuff up

So we have a few events we need to wire up, Loaded, StateChanged, and IsVisibleChanged.

On load, we need to do a few things.

C#

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    SetWindowLocation();
    Win32.HideFromAltTab(handle);
    
    Width = SystemParameters.FullPrimaryScreenWidth;
    Top = SystemParameters.PrimaryScreenHeight - Height - 10;

    CreateNotifyIcon();
    VerifyToDoFileLocation();

    if (File.Exists(TodoFileFullPath))
        fillWrapPanel();

    createFileSystemWatcher();
}

VB

Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
    SetWindowLocation()
    Win32.HideFromAltTab(Handle)

    Width = SystemParameters.FullPrimaryScreenWidth
    Top = SystemParameters.FullPrimaryScreenHeight - Height + 22

    CreateNotifyIcon()
    VerifyToDoFileLocation()

    If File.Exists(TodoFileFullPath) Then
        fillWrapPanel()
    End If

    createFileSystemWatcher()
End Sub

State change is fired when an end user does something like minimize or maximize.  Here if the application is minimized in some edge case, we'll just force it back to being visible.

C#

if (WindowState == WindowState.Minimized)
    WindowState = WindowState.Normal;

VB

If WindowState = WindowState.Minimized Then
    WindowState = WindowState.Normal
End If

Time to use some Windows Forms

When I created this application a bit ago, WPF didn't have a system tray control, otherwise known as a NotifyIcon control.  This may have changed but we'll just use the Windows Form NotifyIcon instead.  Since this is a .Net application, gaining access is rather easy but we'll have to set all the properties by hand.  We'll also have to create the context menu by hand that will be associated with this.  We'll need to add in a reference to System.Windows.Forms but we'll rename it to forms when calling it to prevent namespace collisions.  We have to do this as there are items called the same in other namespaces.  Now this aspect of the application will be replaced with the Coding4Fun Util Runner with MEF in the future.

C#

private void CreateNotifyIcon()
{
    _notifyIcon = new forms.NotifyIcon { Text = "ToDo", Icon = new Icon("report.ico"), Visible = true };

    var contextMenu = new forms.ContextMenuStrip { Name = "contextMenu" };
    var exitToolStripMenuItem = new forms.ToolStripMenuItem { Name = "exitToolStripMenuItem1", Text = "Exit" };
    var configurationMenuItem = new forms.ToolStripMenuItem { Name = "configurationMenuItem", Text = "Options" };
    var openMenuItem = new forms.ToolStripMenuItem { Name = "openMenuItem", Text = "Open ToDo" };

    // contextMenu
    contextMenu.Items.AddRange(new forms.ToolStripItem[] { openMenuItem, configurationMenuItem, exitToolStripMenuItem });
    contextMenu.Size = new Size(155, 114);

    exitToolStripMenuItem.Click += exitToolStripMenuItem_Click;
    configurationMenuItem.Click += configurationMenuItem_Click;
    openMenuItem.Click += openMenuItem_Click;
    _notifyIcon.ContextMenuStrip = contextMenu;
}

VB

Private Sub CreateNotifyIcon()
    _notifyIcon = New _forms.NotifyIcon With {.Text = "ToDo", .Icon = New Icon("report.ico"), .Visible = True}

    Dim contextMenu = New Forms.ContextMenuStrip With {.Name = "contextMenu"}
    Dim exitToolStripMenuItem = New Forms.ToolStripMenuItem With {.Name = "exitToolStripMenuItem1", .Text = "Exit"}
    Dim configurationMenuItem = New Forms.ToolStripMenuItem With {.Name = "configurationMenuItem", .Text = "Options"}
    Dim openMenuItem = New Forms.ToolStripMenuItem With {.Name = "openMenuItem", .Text = "Open ToDo"}

    ' contextMenu
    contextMenu.Items.AddRange(New Forms.ToolStripItem() {openMenuItem, configurationMenuItem, exitToolStripMenuItem})
    contextMenu.Size = New Size(155, 114)

    AddHandler exitToolStripMenuItem.Click, AddressOf exitToolStripMenuItem_Click
    AddHandler configurationMenuItem.Click, AddressOf configurationMenuItem_Click
    AddHandler openMenuItem.Click, AddressOf openMenuItem_Click
    _notifyIcon.ContextMenuStrip = contextMenu
End Sub

PInvoking to the back of the class

While .Net can do tons of stuff, at times you'll need to do some magic with native APIs.  You can find the extra pinvoke declarations at www.pinvoke.net.  For this, we need to do a pinvoke call to hide from ALT-Tab along with forcing the application to the back of the desktop.  I created a class that calls these in a friendly way so all my pinvokes are private methods.

For hiding from ALT-Tab, here is how you'd execute that call.

C#

[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr window, int index, int value);

[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr window, int index);

// alt tab code from http://bytes.com/forum/thread442047.html
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOOLWINDOW = 0x00000080;

public static void HideFromAltTab(IntPtr Handle)
{
    SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) | WS_EX_TOOLWINDOW);
}

VB

<DllImport("user32.dll")> _
Private Shared Function SetWindowLong(ByVal window As IntPtr, ByVal index As Integer, ByVal value As Integer) As Integer
End Function

<DllImport("user32.dll")> _
Private Shared Function GetWindowLong(ByVal window As IntPtr, ByVal index As Integer) As Integer
End Function

' alt tab code from http://bytes.com/forum/thread442047.html
Private Const GWL_EXSTYLE As Integer = -20
Private Const WS_EX_TOOLWINDOW As Integer = &H80

Public Shared Sub HideFromAltTab(ByVal Handle As IntPtr)
    SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) Or WS_EX_TOOLWINDOW)
End Sub

Public Shared Sub SetProcessWorkingSetSize()
    SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle, -1, -1)
End Sub

Setting the window position requires a bit more work.

C#

// window positioning flags
static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
static readonly IntPtr HWND_TOP = new IntPtr(0);
static readonly IntPtr HWND_BOTTOM = new IntPtr(1);

public const uint SWP_NOMOVE = 0x2;
public const uint SWP_NOSIZE = 0x1;
public const uint SWP_SHOWWINDOW = 0x40;

// http://www.developer.com/net/csharp/article.php/3347251
// and http://msdn.microsoft.com/en-us/library/ms633545(VS.85).aspx
// AND http://pinvoke.net/default.aspx/user32.SetWindowPos
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern bool SetWindowPos(
    IntPtr hWnd, // window handle
    IntPtr hWndInsertAfter, // placement-order handle
    int X, // horizontal position
    int Y, // vertical position
    int cx, // width
    int cy, // height
    uint uFlags);

public static void SetWindowInBack(IntPtr handle)
{
    SetWindowPos(handle,
       HWND_NOTOPMOST,
       0, 0, 0, 0,
       SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);

    SetWindowPos(handle,
       HWND_BOTTOM,
       0, 0, 0, 0,
       SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
}

public static void SetWindowInFront(IntPtr handle)
{
    SetWindowPos(handle,
       HWND_TOPMOST,
       0, 0, 0, 0,
       SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
}

VB

' window positioning flags
Shared ReadOnly HWND_TOPMOST As New IntPtr(-1)
Shared ReadOnly HWND_NOTOPMOST As New IntPtr(-2)
Shared ReadOnly HWND_TOP As New IntPtr(0)
Shared ReadOnly HWND_BOTTOM As New IntPtr(1)

Public Const SWP_NOMOVE As UInteger = &H2
Public Const SWP_NOSIZE As UInteger = &H1
Public Const SWP_SHOWWINDOW As UInteger = &H40

' http://www.developer.com/net/csharp/article.php/3347251
' and http://msdn.microsoft.com/en-us/library/ms633545(VS.85).aspx
' AND http://pinvoke.net/default.aspx/user32.SetWindowPos

<DllImport("user32.dll", EntryPoint:="SetWindowPos")> _
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, _
 ByVal uFlags As UInteger) As Boolean
End Function

Public Shared Sub SetWindowInBack(ByVal Handle As IntPtr)
    SetWindowPos(Handle, HWND_NOTOPMOST, 0, 0, 0, 0, _
     SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)

    SetWindowPos(Handle, HWND_BOTTOM, 0, 0, 0, 0, _
     SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
End Sub

Public Shared Sub SetWindowInFront(ByVal Handle As IntPtr)
    SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, _
     SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
End Sub

Watching files for doing some sneaky stuff

In .Net, there is a fantastic class called the FileSystemWatcher which will watch a folder or file for you and tells you when anything interesting happened.  For this instance, we're interested if a file was created or altered.  So when that file has been updated by someone adding or removing text from it, an event will be raised.  Since event will occur on a non-UI thread, we have to tell the Dispatcher object on the form to update the by using the fillWrapPanel function.  In a Win32 application, you could get away without doing this however in WPF, you cannot do that type of cross threading.

C#

private void createFileSystemWatcher()
{
    todoWatcher = new FileSystemWatcher { Path = Settings.Default.ToDoFilePath, Filter = Settings.Default.ToDoFileName };

    todoWatcher.Changed += todoWatcher_Changed;
    todoWatcher.Created += todoWatcher_Changed;

    todoWatcher.EnableRaisingEvents = true;
}

private void todoWatcher_Changed(object sender, FileSystemEventArgs e)
{
    wpTodo.Dispatcher.Invoke(new NoArgDelegate(fillWrapPanel));
}

VB

Private Sub createFileSystemWatcher()
    todoWatcher = New FileSystemWatcher With {.Path = My.Settings.ToDoFilePath, .Filter = My.Settings.ToDoFileName}

    AddHandler TodoWatcher.Changed, AddressOf todoWatcher_Changed
    AddHandler TodoWatcher.Created, AddressOf todoWatcher_Changed

    TodoWatcher.EnableRaisingEvents = True
End Sub

Private Sub todoWatcher_Changed(ByVal sender As Object, ByVal e As FileSystemEventArgs)
    wpTodo.Dispatcher.Invoke(New NoArgDelegate(AddressOf fillWrapPanel))
End Sub

Filling Panels with glowing goodness

Updating the panel is pretty straight forward.  You create new Label objects and just add them in.  We'll split the todo file based off the new line character and then each line will be a Label.  We'll also apply an OuterGlowBitmapEffect to make it so you can see the words easier.

C#

private void fillWrapPanel()
{
    // does file exist and not in use?
    string todoFileText = string.Empty;

    if (File.Exists(TodoFileFullPath))
    {
        while (FileInUse(TodoFileFullPath))
            Thread.Sleep(50);
        
        todoFileText = File.ReadAllText(TodoFileFullPath);
    }

    wpTodo.Children.Clear();
    string[] items = Regex.Split(todoFileText, Environment.NewLine);

    foreach (string item in items)
        wpTodo.Children.Add(CreateLabel(item));
}

private Label CreateLabel(string item)
{
    var txt = new Label
    {
        Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(0, 0, 0, 0)),
        Foreground = new SolidColorBrush(Settings.Default.FontForeColor),
        AllowDrop = false,

        Focusable = false,
        BorderThickness = new Thickness(0),
        Content = item
    };
    txt.MouseDown += txt_MouseDown;

    if (Settings.Default.FontGlowColor.A != 0)
        txt.BitmapEffect = new OuterGlowBitmapEffect
        {
            GlowColor = Settings.Default.FontGlowColor,
            GlowSize = 7
        };

    return txt;
}

void txt_MouseDown(object sender, MouseButtonEventArgs e)
{
    SetWindowLocation();
}

VB

Private Sub fillWrapPanel()
    ' does file exist and not in use?
    Dim todoFileText As String = String.Empty

    If File.Exists(TodoFileFullPath) Then
        While FileInUse(TodoFileFullPath)
            Thread.Sleep(50)
        End While

        todoFileText = File.ReadAllText(TodoFileFullPath)
    End If

    wpTodo.Children.Clear()
    Dim items As String() = Regex.Split(todoFileText, Environment.NewLine)

    For Each item As String In items
        wpTodo.Children.Add(CreateLabel(item))
    Next
End Sub

Private Function CreateLabel(ByVal item As String) As Label

    Dim txt As New Label() With { _
      .Background = New SolidColorBrush(System.Windows.Media.Color.FromArgb(0, 0, 0, 0)), _
      .Foreground = New SolidColorBrush(My.Settings.FontForeColor), .AllowDrop = False, _
      .Focusable = False, .BorderThickness = New Thickness(0), .Content = item}

    AddHandler txt.MouseDown, AddressOf txt_MouseDown

    If My.Settings.FontGlowColor.A <> 0 Then
        txt.BitmapEffect = New OuterGlowBitmapEffect() With {.GlowColor = My.Settings.FontGlowColor, .GlowSize = 7}
    End If

    Return txt
End Function

Sub txt_MouseDown(ByVal sender As Object, ByVal e As MouseButtonEventArgs)
    SetWindowLocation()
End Sub

Conclusion

This application is just a quick and easy application to make my life saner along with learn WPF.  If you plan on doing a lot more design work with WPF, I would suggest using Expression Blend instead of Visual Studio.

If you want to try this out, the download link for the source code is at the top of the article!

About The Author

Clint Rutkas is an academic developer evangelist for Microsoft.  His two primary development languages are C# and JavaScript. In his off time, he whips up other random weird projects and does twenty something activities with his friends.  In the past he has built a computer controlled disco dance floor, an automated bartender and a skateboard segway all powered by c#. His blog http://www.betterthaneveryone.com talks about how these items were built and the reasons why they were built like they are. If you want, he is on twitter @ClintRutkas or you can email him at clint.rutkas@microsoft.com if you have any question.

Tag:

Follow the Discussion

  • DougDoug

    I like it! Excellent job.

    Is there a way to prevent Show Desktop or Windows Key + D from hiding the list?

    Thanks!

  • Mark RaineyMark Rainey

    A suggested minor enhancement ...

    Adding the following line to CreateNotifyIcon

      _notifyIcon.MouseDoubleClick += openMenuItem_Click;

    allows double clicking on the icon to launch the editing of the todo list, saving having to bring up the menu each time.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.