Share via


Creating a Visual Studio .NET Add-In to Provide Windows XP Theme Support

 

Duncan Mackenzie
Microsoft Developer Network

(See Duncan's profile on GotDotNet.)

July 2002

Applies to:
   Microsoft® Visual Studio® .NET
   Microsoft® Windows® XP
   Windows Forms

Summary: Use Windows Forms to create an add-in for Microsoft Visual Studio .NET that automatically sets up your code to use the look of Microsoft Windows XP. (26 printed pages)

Download XPThemeSupport.exe.

Note   This sample is based on code by Mark Boulter, Program Manager for Windows Forms.

Contents

Introduction
Designing the Add-In
Breaking Down the Implementation
Implementing the Theme Support Functions
Summary

Introduction

Some of the common user-interface elements of Microsoft Windows have a new look under Windows XP—see Figure 1 (new style) and Figure 2 (old style)—but not all aspects of this new appearance will be automatically applied to your applications.

Figure 1. A Windows Forms application displayed using XP Themes

Figure 2. The same form displayed without theme support

To avoid compatibility issues (because the XP style controls are part of a new version of the Windows Common Controls), applications do not automatically use the XP styles. Instead, an application that wishes to link to the updated version of the Common Controls needs to explicitly state this desire through the use of a manifest file (or through a special resource embedded into the application).

Note   Manifest files are special XML documents used to specify that an application can or wishes to use a certain version of a library, when more than one version of that library is capable of being installed at the same time. In the case of the XP user-interface styles, the library in question is the Windows Common Controls, and the old pre-XP controls have been installed side-by-side (meaning both versions are installed at the same time) with the new XP-themed styles. Check out this reference for more information.

That manifest file must be named correctly (<your .exe name>.manifest) and must be in the same directory as the .exe file for your application to take on the correct appearance. Creating the file is easy, as the contents (documented here) are the same for any application, but copying it into the correct directory and making sure it has the correct name every time you build is a bit of a hassle. This article shows you how this task can be made a lot simpler while teaching you about creating a Microsoft Visual Studio .NET add-in at the same time.

Designing the Add-In

The detailed requirements to make your application use Windows XP-themed controls are covered quite well in Seth Grossman's article, Using Windows XP Visual Styles with Controls on Windows Forms, so I won't go into great detail on them here. The main issue is to ensure that a correctly named manifest file, containing accurate information, is in the same directory as your .exe file. A secondary part of the problem is that for some controls, the control only takes on the XP style appearance if its FlatStyle property is set to System. To handle both of these issues, I will have to create a Visual Studio .NET add-in that can respond to Build events, to copy the manifest file whenever the project is compiled, and that can somehow set the FlatStyle property to System for those controls that need it.

In addition to those two requirements, I will also need to add and manipulate a couple of menu items. Through the use of these menu items, the user can selectively enable or disable my add-in for specific projects, since not every project will be targeted at Windows XP. Creating the UI elements, and controlling their visibility based on the current selected objects in Visual Studio (you wouldn't want them available unless a project was selected), will actually add as much code to my final solution as the previous two requirements together.

Breaking Down the Implementation

Based on the previous requirements discussion, I now know what development tasks I will need to accomplish in my add-in. For the most part, the following tasks are useful in many different add-in projects:

  • Adding menu items to Visual Studio .NET
  • Determining if a project is currently selected
  • Manipulating those items
  • Responding to clicks on those items
  • Adding your own properties to Visual Studio .NET projects
  • Responding to events raised by Visual Studio .NET

Some tasks are specific to my Theme Support example, though, so I will cover those separately at the end of this implementation section:

  • Setting control properties from your add-in
  • Creating/copying a manifest file into the output directory

Each of these tasks will be covered individually, but before I can get to all that fancy stuff, I have to create my Visual Studio .NET Add-In project.

Getting Started

To create an add-in project, a pair of handy wizards is provided under Other Projects\Extensibility Projects. If your goal is to create an add-in for just Visual Studio .NET, as opposed to one that can be hosted in any one of a set of applications that support the same add-in model, you should choose the Visual Studio .NET Add-In project. Although either one of these items will work, (both can create add-ins to be hosted inside Visual Studio .NET), the code produced by the Visual Studio-specific wizard is slightly easier to use. Because it is only targeting one host, the wizard can use strongly typed variables in places where the Shared Add-In wizard has to use Object.

To get started on my Theme Support add-in, I chose the Visual Studio .NET add-in type, set the project name to "XPThemeSupport," and then clicked OK. This started a wizard, where I could pick the language in which I wanted to write my add-in (Microsoft Visual Basic®, C#, or C++, depending on the languages you have installed) and the Visual Studio hosts (your choices would normally be Visual Studio itself, or the Visual Studio Macro development environment) in which I want my add-in to appear. For my language, I chose Visual Basic .NET, but feel free to pick whatever language you wish, as any of these will work. For hosts, select just the Visual Studio .NET option to register the add-in to run inside the main Visual Studio development environment.

Note   Although I wrote this add-in in Visual Basic .NET, it works with any type of Visual Studio project, including both Visual Basic .NET and C#.

You will need to supply a name and description, and then set a few options. I chose to only select "I would like my add-in to load when the host application starts," and leave everything else unchecked. Although I do want some menu items, I still don't select the option to create a Tools menu item, because if I am adding multiple items I find it easier to write my own code. Another key option on this dialog involves command-line builds; you cannot support command-line building if your application might put up a modal dialog (including a message box). Any form of modal UI would stop the command-line build and force it to wait for user interaction before it could continue; not the best behavior for an automated build in the middle of the night.

Note   The options you specify in this wizard end up stored in the registry at HKCU\Software\Microsoft\VisualStudio\7.0\AddIns\<your add in name>.Connect, and are set up as part of the Add-In install project that will be added to your solution at the completion of this wizard.

Next, you get to specify some information describing your add-in, which will then appear in Visual Studio's own About dialog box, as its own entry complete with icon and your description. Don't use returns, as they won't show up correctly in the Visual Studio About box, just trust in the power of word wrap.

Figure 3. Your about box information is displayed as part of the Visual Studio About dialog box.

That is it for the wizard. Clicking Finish after entering your about box information will exit the wizard and generate the basic add-in code (and a complete install project) based on your selections. At this point you have the complete skeleton code for your project, which I will briefly discuss next, and you can start adding your specific functionality.

Understanding the Add-In Code

The code created by the Add-In wizard consists of a single class, Connect, which implements the IDTExtensibility2 interface. Implementing this interface is what allows host applications to load this class as an add-in, and all interaction between the host and your code starts out through the methods of this interface. While all of the methods are important, it is possible (and I'm going to) implement an entire add-in only using the OnConnection method. This method is called when Visual Studio .NET loads your add-in, and it is the perfect place to handle adding your menu items and to start adding event handlers for all of the integrated development environment (IDE) events you are interested in. The parameters to OnConnection include the host application, which I know will be an instance of EnvDTE.DTE (which represents Visual Studio), since this add-in can only be hosted within one application. If you are creating a shared add-in, this application object could be the root object of any one of several different applications, so you wouldn't be able to cast it to a strongly typed variable. For readability, I am not going to do any work in the OnConnection method directly; instead, I am going to write another class containing all of my custom code and then create an instance of that class in the OnConnection method. My new class will need access to the application object passed into OnConnection, so I will make that a required part of the class constructor.

Public Class ThemeSupport
    Private hostApplication As EnvDTE.DTE

    Public Sub New(ByVal application As EnvDTE.DTE)
        hostApplication = application
        CreateMenuItems()
        UpdateUI()
        SetupEvents()
    End Sub
End Class

I will then pass the application object when I am creating an instance of the class in the OnConnection method.

Public Sub OnConnection(ByVal application As Object, _
        ByVal connectMode As Extensibility.ext_ConnectMode, _
        ByVal addInInst As Object, _
        ByRef custom As System.Array) _
        Implements IDTExtensibility2.OnConnection
    'Declaring my class is enough, as its constructor 
    'kicks off all the objects I need.
    Dim theme As New ThemeSupport(CType(application, EnvDTE.DTE))
End Sub

Since I will be handling everything in my own class, I have removed the local variables (applicationObject and addInInstance) from the auto-generated code, replacing the entire method with a single line that creates an instance of my ThemeSupport class.

Creating the Menu Items

The Visual Studio menus and toolbars use the Microsoft Office object model, where toolbars are represented by CommandBar objects and individual menu/toolbar items are CommandBarButton objects. To add my two menu items, I am going to create two class-level CommandBarButton variables in my ThemeSupport class, each declared with the WithEvents keyword so that I can easily handle their Click events. Immediately after my class is created, I will create these two buttons and set up their properties appropriately.

Private WithEvents onButton As CommandBarButton
Private WithEvents offButton As CommandBarButton

Private Sub CreateMenuItems()
    Dim toolsMenu As CommandBar
    toolsMenu = hostApplication.CommandBars("Tools")

    onButton = toolsMenu.Controls.Add( _
            Type:=MsoControlType.msoControlButton, _
            Temporary:=True)

    onButton.Caption = "Turn Theme Support On"
    onButton.Visible = False
    onButton.BeginGroup = True

    offButton = toolsMenu.Controls.Add( _
            Type:=MsoControlType.msoControlButton, _
            Temporary:=True)
    offButton.Caption = "Turn Theme Support Off"
    offButton.Visible = False
End Sub

I have created two menu items, labeled Turn Theme Support On and Turn Theme Support Off, respectively, but only one of these two items will ever be visible at any given time. Notice that in the call to Controls.Add, I specify Temporary:=True, this tells Visual Studio that these controls should be removed when it closes, which forces me to re-create them every time, but helps prevent the possibility of duplicate menu items or menu items that hang around even if the add-in has been uninstalled. As I mentioned earlier, the Visual Studio menus and toolbars are actually represented using the Office object model, which is not documented in the Visual Studio Help files. An online reference to this object model, which details each of the objects along with their properties and methods, is available on MSDN.

The next trick is to watch the currently selected items in Visual Studio, determine whether a project is selected, and to adjust the appearance of the two menu items accordingly.

Examining Visual Studio's Current Selection

Since I need to check if a project is selected in a few different places in my code, I decided to create an IsProjectSelected() function that would return True if a single project or an item from a single project is currently selected. The Visual Studio DTE object exposes a SelectedItems collection, which I used to create a GetCurrentlySelectProject() function that would return either the current project or Nothing if no project is selected.

Private Function GetCurrentlySelectedProject() As EnvDTE.Project
    Dim selItems As EnvDTE.SelectedItems
    Dim selItemObj, selItem As EnvDTE.SelectedItem
    Dim selProject As EnvDTE.Project
    selItems = hostApplication.SelectedItems
    ' List the number of items selected.
    If selItems.Count > 0 And Not selItems.MultiSelect Then
        ' Set a reference to the first selected item.
        selItemObj = selItems.Item(1)
        ' List the names of the project
        ' or project items under the selected 
        ' item.
        For Each selItem In selItemObj.Collection
            If TypeOf selItem.Project Is EnvDTE.Project Then
                selProject = selItem.Project
            Else
                If TypeOf selItem.ProjectItem _
                    Is EnvDTE.ProjectItem _
                        AndAlso _
                   TypeOf selItem.ProjectItem.ContainingProject _
                    Is EnvDTE.Project Then
                    selProject = selItem.ProjectItem.ContainingProject
                End If
            End If
        Next
    End If
    Return selProject
End Function

Once that code was done, it was easy to use it to create the IsProjectSelected() function.

Private Function IsProjectSelected() As Boolean
    If Not GetCurrentlySelectedProject() Is Nothing Then
        Return True
    Else
        Return False
    End If
End Function

Setting and Retrieving User-Defined Properties in Projects

With IsProjectSelected() available, the next thing I needed was a way to store a setting into each individual project, to turn theme support on or off. Luckily, this is a need that the Visual Studio team seemed to expect, and they provided a set of functions to set and retrieve values associated with any particular project. These functions, members of the Globals property of the Project object, allow you to check if a variable has been defined (VariableExists), set a variable to save with the project file (VariablePersists), and to retrieve or set the value of the variable (VariableValue). A persisted value is added to the XML of the project file, like this:

<UserProperties Themed = "TRUE" />

Using these functions and a variable name (which can be any string value) of "Themed," I created three procedures for my add-in: IsProjectThemed, TurnProjectThemingOn, and TurnProjectThemingOff (which is not listed to save space, but is basically the same as TurnProjectThemingOn).

Private Function IsProjectThemed _
        (ByVal proj As EnvDTE.Project) As Boolean
    If Not proj Is Nothing Then
        Try
            With proj.Globals
                If .VariableExists(variableName) Then
                    Return (.VariableValue(variableName) = "TRUE")
                Else
                    'This is essentially the default case
                    Return False
                End If
            End With
        Catch e As Exception
            Return False
        End Try
    End If
End Function

Private Sub TurnProjectThemingOn()
    Dim selProject As EnvDTE.Project _
        = GetCurrentlySelectedProject()
    Dim projectGlobals As EnvDTE.Globals
    If Not selProject Is Nothing Then
        projectGlobals = selProject.Globals
        With projectGlobals
            If .VariableExists(variableName) Then
                If CStr(.VariableValue(variableName)) = "FALSE" Then
                    .VariableValue(variableName) = "TRUE"
                End If
            Else
                .VariableValue(variableName) = "TRUE"
                .VariablePersists(variableName) = True
            End If
        End With
    End If
End Sub

With the IsProjectThemed() and IsProjectSelected() functions written, I could write a procedure that would update the visibility of the two menu items accordingly. This procedure, UpdateUI, will hide both menu items unless a project is selected, and it will show the Turn Theme Support Off menu item if the project is already themed and Turn Theme Support On menu item if the project isn't themed.

Private Sub UpdateUI()
    If IsProjectSelected() Then
        Dim selProject As EnvDTE.Project _
            = GetCurrentlySelectedProject()
        If IsProjectThemed(selProject) Then
            onButton.Visible = False
            offButton.Visible = True
        Else
            onButton.Visible = True
            offButton.Visible = False
        End If
    Else
        onButton.Visible = False
        offButton.Visible = False
    End If
End Sub

Handling IDE Events

Visual Studio's object model exposes a wide variety of events, all of which are available through the members of the Events class. For the purpose of showing and hiding my menu items, I was interested in Events.SelectionEvents.OnChange, which fires whenever the currently selected item changes in Visual Studio. To trap this event, I created a member variable in my ThemeSupport class (selEvents) of type SelectionEvents and declared WithEvents. Once that variable is declared, I can use the drop-down menus in the IDE to create an event handler for the OnChange event.

Private WithEvents selEvents As EnvDTE.SelectionEvents

Private Sub selEvents_OnChange() Handles selEvents.OnChange
    UpdateUI()
End Sub

For this event handler to be called, though, I need to make sure that my member variable selEvents is initialized to the real DTE.Events.SelectionEvents object from Visual Studio .NET. Since I am actually going to be capturing selection, window, and Build events, I set up all three of my event member variables in a routine called SetupEvents that is called from the constructor of my ThemeSupport class.

Private Sub SetupEvents()
    selEvents = hostApplication.Events.SelectionEvents
    buildEvents = hostApplication.Events.BuildEvents
    winEvents = hostApplication.Events.WindowEvents
End Sub

Responding to Menu Item Selection

Now that I have finally written all the code to determine which menu item should be visible, and to show and hide them appropriately, I can finally move on to the actual code for when one of these menu items is selected. Each item is declared as WithEvents inside my ThemeSupport class, so it is relatively easy to add code into the Click event for each item. Now that I have the TurnProjectThemingOn and TurnProjectThemingOff routines created (as discussed earlier), the code for these two Click events is relatively simple.

Private Sub onButton_Click( _
        ByVal Ctrl As CommandBarButton, _
        ByRef CancelDefault As Boolean) _
    Handles onButton.Click
    TurnProjectThemingOn()
    selEvents_OnChange()
End Sub

Private Sub offButton_Click( _
        ByVal Ctrl As CommandBarButton, _
        ByRef CancelDefault As Boolean) _
    Handles offButton.Click
    TurnProjectThemingOff()
    selEvents_OnChange()
End Sub

Now, with all of this code written, I still haven't done anything to add theme support into my project! All I have done is created some menu items and added a custom project-level property, all of which could really apply to almost any add-in project. With all that plumbing in place, though, I can finally start getting into the theme-specific work.

Implementing the Theme Support Functions

Just to recap briefly, to add theme support to a project, I set up a project-level property named "Themed," and if that property is set to True, I want to make that project take on the appropriate Windows XP appearance. I will handle the XP appearance by copying a manifest file into the build directory, making sure that the FlatStyle property is correctly set for my Windows Forms controls.

Copying the Manifest File

I could create the manifest file on the fly at run time, writing out the necessary text into a file, but a more maintainable approach is to use an actual file on disk as a template. Using an actual file allows you to change that file if necessary, changing the manifest file this add-in will produce without having to change any code. A Default.manifest file will be used and is assumed to be placed in the same directory as my add-in's DLL. (The provided Setup project will take care of this step if you use it to install the add-in.) Inside the default manifest (shown next), I have used placeholders ("[APPLICATION_NAME]," "[APPLICATION_DESCRIPTION]," and "[APPLICATION_VERSION]") for the values that are application specific.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity version="[APPLICATION_VERSION]" processorArchitecture="X86" name="[APPLICATION_NAME]"
    type="win32" /> <description>[APPLICATION_DESCRIPTION]</description> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="X86" publicKeyToken="6595b64144ccf1df" language="*" /> </dependentAssembly> </dependency> </assembly>

At build time, when I copy over the manifest file, I will replace these placeholders with the actual values from my application. The manifest copying and string replacement procedure is encapsulated into the CopyManifest procedure in my code, which is called in response to one Visual Studio's Build events (OnBuildProjConfigDone).

Private Sub buildEvents_OnBuildProjConfigDone( _
        ByVal Project As String, _
        ByVal ProjectConfig As String, _
        ByVal Platform As String, _
        ByVal SolutionConfig As String, _
        ByVal Success As Boolean) _
    Handles buildEvents.OnBuildProjConfigDone

    Dim proj As EnvDTE.Project
    proj = hostApplication.Solution.Projects.Item(Project)
    If Not proj Is Nothing Then
        If IsProjectThemed(proj) Then
            Dim assemblyFilePath As String
            Dim projectFilePath As String
            Dim outputFilePath As String
            Dim outputFileName As String
            Dim manifestFilePath As String
            Dim targetFilePath As String
            Dim config As EnvDTE.Configuration

            projectFilePath = _
            CStr(proj.Properties.Item("FullPath").Value)
            outputFileName = _
                CStr(proj.Properties.Item("OutputFileName").Value)

            If IO.File.Exists(projectFilePath + "app.manifest") Then
                manifestFilePath = projectFilePath + "app.manifest"
            Else
                assemblyFilePath _
                    = Assembly.GetExecutingAssembly.Location
                manifestFilePath = _
                    Path.Combine( _
                        Path.GetDirectoryName(assemblyFilePath), _
                        "default.manifest")
            End If

            config = _
                proj.ConfigurationManager.Item( _
                    ProjectConfig, Platform)
            outputFilePath = _
                CStr(config.Properties.Item("OutputPath").Value)

            If Not Path.IsPathRooted(outputFilePath) Then
                'it is a relative path, to the project directory
                outputFilePath = _
                    Path.Combine(projectFilePath, outputFilePath)
            End If

            outputFileName = Path.Combine(outputFilePath, outputFileName)
            targetFilePath = outputFileName + ".manifest"
            CopyManifest(proj, _
                manifestFilePath, _
                targetFilePath, _
                outputFileName)
        End If
    End If
End Sub

Private Sub CopyManifest(ByVal proj As EnvDTE.Project, _
        ByVal manifestPath As String, _
        ByVal outputPath As String, _
        ByVal outputFilePath As String)

    Dim manifest As String
    Dim manifestStream As New StreamReader(manifestPath)
    Dim outputStream As New StreamWriter(outputPath, False)

    manifest = manifestStream.ReadToEnd
    manifestStream.Close()

    Dim myFileVersionInfo As FileVersionInfo = _
        FileVersionInfo.GetVersionInfo(outputFilePath)

    With myFileVersionInfo
        Dim appDescription As String = .Comments
        Dim appName As String = .FileDescription
        Dim appVersion As String
        appVersion = String.Format("{0}.{1}.{2}.{3}", _
            .FileMajorPart, .FileMinorPart, _
            .FileBuildPart, .FilePrivatePart)
        manifest = _
            manifest.Replace( _
                "[APPLICATION_NAME]", appName)
        manifest = _
            manifest.Replace( _
                "[APPLICATION_DESCRIPTION]", appDescription)
        manifest = _
            manifest.Replace( _
                "[APPLICATION_VERSION]", appVersion)
    End With
    outputStream.Write(manifest)
    outputStream.Close()
End Sub

The work done in the Build event takes care of copying and customizing the manifest, but some controls will only take on the Windows XP appearance if their FlatStyle property is set to System. To make it even easier for you to build XP applications, I added some more functionality to my add-in that will set a control's FlatStyle correctly as soon as you add it to your form.

Setting Control Properties

The trick to setting the FlatStyle of controls is trapping the event that occurs when a control is added. The Visual Studio extensibility library, which has provided me with most of the objects and events I have used so far, does not provide direct access to events occurring in the Windows Forms designer. Luckily, the System.ComponentModel.Design classes in the .NET Framework expose the functionality I need. By hooking into the creation and closing of Windows in the IDE, I can grab each window and cast it to an IDesignerHost object.

Private Sub winEvents_WindowCreated(ByVal Window As EnvDTE.Window) _
        Handles winEvents.WindowCreated
    Dim host As IDesignerHost _
        = CType(Window.Object, IDesignerHost)
    If Not host Is Nothing Then
        Dim ccs As IComponentChangeService
        ccs = host.GetService(GetType(IComponentChangeService))
        AddHandler ccs.ComponentAdded, _
            AddressOf Me.ccsEvents_ComponentAdded
    End If
End Sub
Private Sub winEvents_WindowClosing(ByVal Window As EnvDTE.Window) _
        Handles winEvents.WindowClosing
    Dim host As IDesignerHost _
        = CType(Window.Object, IDesignerHost)
    If Not host Is Nothing Then
        Dim ccs As IComponentChangeService
        ccs = host.GetService(GetType(IComponentChangeService))
        RemoveHandler ccs.ComponentAdded, _
            AddressOf Me.ccsEvents_ComponentAdded
    End If
End Sub

Using the GetService method of the IDesignerHost interface, I can add an event handler that responds to the ComponentAdded event of the IComponentChangeService interface. Then, whenever a component is added to a form, my handler will be called and I can set the FlatStyle property appropriately.

Private Sub ccsEvents_ComponentAdded(ByVal sender As Object, _
    ByVal e As ComponentEventArgs)
    Try
        Dim ctl As Control
        ctl = CType(e.Component, Control)
        If Not ctl Is Nothing Then
            Dim pd As PropertyDescriptor

            pd = TypeDescriptor.GetProperties(ctl).Item("FlatStyle")
            If Not pd Is Nothing Then
                If pd.PropertyType.Equals(GetType(FlatStyle)) Then
                    pd.SetValue(ctl, FlatStyle.System)
                End If
            End If
        End If
    Catch ex As Exception
        'It is possible that a control could have a property 
        'named FlatStyle that wasn't the 'standard' 
        'implementation. So if the property assignment
        'fails, I just want to ignore it.
        Debug.WriteLine(ex.Message)
    End Try
End Sub

Note that this implementation leaves a few holes in terms of setting properties, such as:

  • What about controls added when Theme Support was set to Off?
  • What happens when you turn Theme Support off? (Is the FlatStyle changed to something else?)

I don't see any perfect answer to either of these issues; scanning your entire project and changing all your controls is possible, but it might not be desirable. A similar problem exists with the manifest files; when you turn Theme Support off, the manifest file from a previous build might be still be in your output directory, but I decided against automatically deleting it. Of course, you might want to deal with these issues differently, but this is just a sample, so you can do whatever you wish with the code.

Summary

If your application is running on Windows XP, you want it to look like it was designed for that OS, not like some legacy application that might not be compatible. Making your Windows Forms applications look great in XP isn't difficult, but it is easy to forget your manifest or the correct setting on some of your controls. Creating an add-in for Visual Studio is a great way to increase the productivity of you and/or your team, and can be a good tool for enforcing developer standards.