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.