|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
ContentsIntroductionWindows XP style Explorer Bars/Task Bars/Web Views - call them what you will - are all the rage at the moment. There are many great free Explorer Bar controls available on the net such as Derek Lakin's Collapsible Panel Bar, Darren May's Collapsing Group Control, and Tom Guinther's Full-featured XP Style Collapsible Panel. Most of them don't support Windows XP themes, or if they do, they "fake" it by drawing the gradient/arrows themselves, or by including the necessary images in the assembly. Frustrated by this lack of proper theme support, I decided to make my own. Features
ThemesSo, how can we make use of themes? My first attempt was to take screenshots of the real Explorer Bar in action so I could get the images and colors that I would need. This worked well as long as I only used the Luna themes that come with XP. I was forced to go back to the drawing board. After stumbling across Don Kackman's article on Adding XP Themes to Custom .NET Controls, attempt #2 was to make use of UxTheme.dll. That was until I got to the part where using UxTheme worked as long as you only used the default Blue Luna theme. However, Don came up with a potential solution - get the necessary information from ShellStyle.dll. ShellStyle.dllNow that I knew where to look, the question was "what am I looking for?" TGTSoft (the makers of StyleXP) have a great free program called ResBuilder that allows you to open and modify Windows resource files. Armed with this program, I was able to have a poke around inside ShellStyle.dll.
Figure 1: ShellStyle.dll in ResBuilder The sections I needed were the Before loading the ShellStyle.dll, we need to check whether themes are available: // check if we are using themes. if so, load up the
// appropriate shellstyle.dll
if (UxTheme.AppThemed && LoadShellStyleDll())
{
...
If they are available, then go ahead and load the ShellStyle.dll: ///
/// Loads the ShellStyle.dll into memory as determined by the current
/// system theme
///
private static bool LoadShellStyleDll()
{
// work out the path to the shellstyle.dll according
// to the current theme
string themeName = UxTheme.ThemeName.Substring(0,
UxTheme.ThemeName.LastIndexOf('\\'));
string styleName = themeName + "\\Shell\\" + UxTheme.ColorName;
string stylePath = styleName + "\\shellstyle.dll";
// if for some reason it doesn't exist, use the default
// shellstyle.dll in the windows\system32 directory
if (!File.Exists(stylePath))
{
stylePath = Environment.GetFolderPath(Environment.SpecialFolder.System) +
"\\shellstyle.dll";
}
// attempt to load the shellstyle dll
hModule = LoadLibrary(stylePath);
// return whether we succeeded
return (hModule != IntPtr.Zero);
}
The UIFILESo, what is a UIFILE? The UIFILE is basically a style sheet that tells the Explorer Bar how it should render itself. Below is a small section that shows settings for a special group's titlebar for the Blue Luna theme: button [id=atom(header)]
{
background: rcbmp(110,6,#FF00FF,0,0,1,0);
borderthickness: rect(2,2,2,0);
foreground: white;
fontweight: rcint(10);
padding: rect(10,0,0,0);
animation: rectanglev | s | fast;
}
(For more information on UIFILEs, bfarber.com has a tutorial on how to read a UIFILE). You'll notice that there are two UIFILEs - the first one is for the Explorer Bar, and the second is for the Control Panel. Now that we know which one we want, it is time to read its contents: ///
/// Extracts the UIFILE from the currently loaded ShellStyle.dll
///
public static string GetResourceUIFile()
{
// locate the "UIFILE" resource
IntPtr hResource = FindResource(hModule, "#1", "UIFILE");
// get its size
int resourceSize = SizeofResource(hModule, hResource);
// load the resource
IntPtr resourceData = LoadResource(hModule, hResource);
// copy the resource data into a byte array so we
// still have a copy once the resource is freed
byte[] uiBytes = new byte[resourceSize];
GCHandle gcHandle = GCHandle.Alloc(uiBytes, GCHandleType.Pinned);
IntPtr firstCopyElement = Marshal.UnsafeAddrOfPinnedArrayElement(uiBytes, 0);
CopyMemory(firstCopyElement, resourceData, resourceSize);
// free the resource
gcHandle.Free();
FreeResource(resourceData);
// convert the char array to an ansi string
string s = Marshal.PtrToStringAnsi(firstCopyElement, resourceSize);
return s;
}
Extracting BitmapsAll bitmaps in the UIFILE have the following format: rcbmp(id, stretching, transparency, width, height, size, mirror)
To load a bitmap, we just pass the bitmap ID to the ///
/// Returns a Bitmap from the currently loaded ShellStyle.dll
///
public static Bitmap GetResourceBMP(string resourceName)
{
// find the resource
IntPtr hBitmap = LoadBitmap(hModule, Int32.Parse(resourceName));
// load the bitmap
Bitmap bitmap = Bitmap.FromHbitmap(hBitmap);
return bitmap;
}
The method above works for ordinary bitmaps, but we run into a major problem if the image is a 32bpp PNG - the alpha channel is lost, leaving black areas where the transparency should be.
Figure 2: Alpha channel comparison The following solution to this problem was posted on Derek Lakin's blog: ///
/// Returns a Png Bitmap from the currently loaded ShellStyle.dll
///
public static Bitmap GetResourcePNG(string resourceName)
{
// the resource size includes some header information
// (for PNG's in shellstyle.dll this appears to be the
// standard 40 bytes of BITMAPHEADERINFO).
const int FILE_HEADER_BYTES = 40;
// load the bitmap resource normally to get dimensions etc.
Bitmap tmpNoAlpha = Bitmap.FromResource(hModule, "#" + resourceName);
IntPtr hResource = FindResource(hModule, "#" + resourceName,
(IntPtr) 2 /*RT_BITMAP*/);
int resourceSize = SizeofResource(hModule, hResource);
// initialise 32bit alpha bitmap (target)
Bitmap bitmap = new Bitmap(tmpNoAlpha.Width,
tmpNoAlpha.Height,
PixelFormat.Format32bppArgb);
// load the resource via kernel32.dll (preserves alpha)
IntPtr hLoadedResource = LoadResource(hModule, hResource);
// copy bitmap data into byte array directly
byte[] bitmapBytes = new byte[resourceSize];
GCHandle gcHandle = GCHandle.Alloc(bitmapBytes, GCHandleType.Pinned);
IntPtr firstCopyElement =
Marshal.UnsafeAddrOfPinnedArrayElement(bitmapBytes, 0);
// nb. we only copy the actual PNG data (no header)
CopyMemory(firstCopyElement, hLoadedResource, resourceSize);
FreeResource(hLoadedResource);
// copy the byte array contents back
// to a handle to the alpha bitmap (use lockbits)
Rectangle copyArea = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
BitmapData alphaBits = bitmap.LockBits(copyArea,
ImageLockMode.WriteOnly,
PixelFormat.Format32bppArgb);
// copymemory to bitmap data (Scan0)
firstCopyElement = Marshal.UnsafeAddrOfPinnedArrayElement(bitmapBytes,
FILE_HEADER_BYTES);
CopyMemory(alphaBits.Scan0, firstCopyElement,
resourceSize - FILE_HEADER_BYTES);
gcHandle.Free();
// complete operation
bitmap.UnlockBits(alphaBits);
GdiFlush();
// flip bits (not sure why this is needed at the moment..)
bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
return bitmap;
}
So, how do we know which one to use for each image? Generally speaking, normal bitmaps will use a hexadecimal transparency value, while PNGs will use an integer value. ...
// if the transparency value starts with a #, then the image is
// a bitmap, otherwise it is a 32bit png
if (transparent.StartsWith("#"))
{
// get the bitmap
image = Util.GetResourceBMP(id);
...
}
else
{
// get the png
image = Util.GetResourcePNG(id);
}
...
XPExplorerBarThe
I won't go into great detail about how each of these were implemented as that is what the source code is for, but I will give an insight into some of the more interesting features such as animation. Using XPExplorerBarBefore using the To add the XPExplorerBar.dll to the toolbox, you can either:
and browse for XPExplorerBar.dll and then press OK. You can then drag the controls onto your Form. Note: If you recompile the source code you will need to re-sign XPExplorerBar.dll, as otherwise Visual Studio will throw an exception when you attempt to add it to the toolbox.
You should then be able to add it to the toolbox. TaskPaneThe
Adding Expandos to a TaskPaneThere are two ways to add
Figure 3: Adding Expandos with the property editor Reordering ExpandosDuring design time, you can use the up and down arrow buttons in the Expando Collection Editor to reorder the
Figure 4: Use arrow buttons to reorder Expandos at design time At all other times, the
// Move an Expando to the top of the TaskPane
taskpane.Expandos.MoveToTop(expando);
Using Themes Other Than The Current ThemeThe
TaskPane taskpane = new TaskPane();
// foreverblue.dll lives in the same directory as
// the executable. if it were somewhere else, we
// would need to use "path/to/foreverblue.dll"
taskpane.UseCustomTheme("foreverblue.dll");
Figure 5: XPExplorerBar demo with Windows XP theme Forever Blue on Windows 2000 Custom themes can be found at ThemeXP. Collapsing/Expanding Multiple ExpandosThe
Expando
I'm sure that right about now, you're wondering where I got the name
Adding Controls to an ExpandoThere are two ways to add
Figure 6a (top) and 6b (bottom): Adding Controls with the property editor. Version 3.0 now allows other Reordering ControlsDuring design time, you can use the up and down arrow buttons in the Control Collection Editor to reorder the
Figure 7: Use arrow buttons to reorder Controls at design time At all other times, the
// Move a TaskItem to the top of the Expando
expando.Items.MoveToTop(taskitem);
As of v3.3
Figure 8: Dragging Expandos around a TaskPane Hide/Show ControlsIn order to hide or show items, the
Note: In order for the Note: As of version 3.3 you can batch // stop the following slide animation
// commands from being performed
expando.BeginUpdate();
expando.HideControl(new Control[] {taskItem1, taskItem2});
expando.ShowControl(taskItem3);
// now perform the animations
expando.EndUpdate();
Note: At the moment the Docking and Scrolling
Figure 9: A docked scrollable Panel As of version 3.0, To add scrolling, simply add a scrollable To stop child controls from covering the title bar and borders when docked, I overrode the ///
/// Overrides DisplayRectangle so that docked controls
/// don't cover the titlebar or borders
///
public override Rectangle DisplayRectangle
{
get
{
return new Rectangle(this.Border.Left,
this.HeaderHeight + this.Border.Top,
this.Width - this.Border.Left - this.Border.Right,
this.ExpandedHeight - this.HeaderHeight -
this.Border.Top - this.Border.Bottom);
}
}
Animation
Figure 10: Animated collapse in action To enable collapse/expand animation, the /// Gets or sets whether the Expando is allowed to animate.
public bool Animate
{
get
{
return this.animate;
}
set
{
this.animate = value;
}
}
When the /// Gets or sets whether the Expando is collapsed.
public bool Collapsed
{
...
set
{
if (this.collapsed != value)
{
// if we're supposed to collapse, check if we can
if (value && !this.CanCollapse)
{
// looks like we can't so time to bail
return;
}
this.collapsed = value;
// only animate if we're allowed to, we're not in
// design mode and we're not initialising
if (this.Animate && !this.DesignMode && !this.Initialising)
{
...
If the ...
this.animationHelper = new AnimationHelper(this,
AnimationHelper.FadeAnimation);
this.OnStateChanged(new ExpandoEventArgs(this));
this.animationHelper.StartAnimation();
...
}
}
}
}
///
/// Starts the animation for the specified expando
///
protected void StartAnimation()
{
// don't bother going any further if we are already animating
if (this.Animating)
{
return;
}
this.animationStepNum = 0;
// tell the expando to get ready to animate
if (this.AnimationType == FadeAnimation)
{
this.expando.StartFadeAnimation();
}
else
{
this.expando.StartSlideAnimation();
}
// start the animation timer
this.animationTimer.Start();
}
Once the ///
/// Gets the Expando ready to start its collapse/expand animation
///
protected void StartAnimation()
{
this.animating = true;
// stop the layout engine
this.SuspendLayout();
// get an image of the client area that we can
// use for alpha-blending in our animation
this.animationImage = this.GetAnimationImage();
// set each control invisible (otherwise they
// appear to slide off the bottom of the group)
foreach (Control control in this.Controls)
{
control.Visible = false;
}
// restart the layout engine
this.ResumeLayout(false);
}
///
/// Returns an image of the group's display area to be used
/// in the animation
///
internal Image GetAnimationImage()
{
// create a new image to draw into
Image image = new Bitmap(this.Width, this.Height);
// get a graphics object we can draw into
Graphics g = Graphics.FromImage(image);
IntPtr hDC = g.GetHdc();
// some flags to tell the control how to draw itself
IntPtr flags = (IntPtr) (WmPrintFlags.PRF_CLIENT |
WmPrintFlags.PRF_CHILDREN |
WmPrintFlags.PRF_ERASEBKGND);
// tell the control to draw itself
NativeMethods.SendMessage(this.Handle,
WindowMessageFlags.WM_PRINT,
hDC, flags);
// clean up resources
g.ReleaseHdc(hDC);
g.Dispose();
// return the completed animation image
return image;
}
///
/// The SendMessage function sends the specified message to a
/// window or windows. It calls the window procedure for the
/// specified window and does not return until the window
/// procedure has processed the message
///
[DllImport("User32.dll")]
internal static extern int SendMessage(IntPtr hwnd,
int msg,
IntPtr wParam,
IntPtr lParam);
The ///
/// Paints the "Display Rectangle". This is the dockable
/// area of the control (ie non-titlebar/border area).
///
protected void PaintDisplayRect(Graphics g)
{
// are we animating
if (this.animating && this.animationImage != null)
{
// calculate the transparency value for the animation image
float alpha = (((float) (this.Height - this.HeaderHeight)) /
((float) (this.ExpandedHeight - this.HeaderHeight)));
float[][] ptsArray = {new float[] {1, 0, 0, 0, 0},
new float[] {0, 1, 0, 0, 0},
new float[] {0, 0, 1, 0, 0},
new float[] {0, 0, 0, alpha, 0},
new float[] {0, 0, 0, 0, 1}};
ColorMatrix colorMatrix = new ColorMatrix(ptsArray);
ImageAttributes imageAttributes = new ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix,
ColorMatrixFlag.Default,
ColorAdjustType.Bitmap);
// work out how far up the animation image we need to start
int y = this.animationImage.Height - this.PseudoClientHeight
- this.Border.Bottom;
// draw the image
g.DrawImage(this.animationImage,
new Rectangle(0, this.HeaderHeight, this.Width,
this.Height - this.HeaderHeight),
0,
y,
this.animationImage.Width,
this.animationImage.Height - y,
GraphicsUnit.Pixel,
imageAttributes);
}
else
{
...
}
}
TaskItem
Figure 11: A TaskItem
Points of InterestSerializationAs of v3.2, Binary Serialization support has been reworked and XML Serialization support has been added. Note: v3.2.1 adds a Version property to the serialization process to ensure backward compatibility with future versions. Anyone using serialization is encouraged to upgrade to v3.2.1 In order to fix the problems that v3.1 had with serialization, I needed to find a new way to perform serialization. After much Googling, I came across the concept of surrogates. A surrogate is a class that will be serialized in place of another class (usually because the other class is either not serializable or contains classes that are not serializable or cause serialization problems).
All the above Surrogates have the following methods for importing/exporting data to/from a Surrogate:
The example below shows how a // BINARY SERIALIZATION
// serialize a TaskPane to a file
IFormatter formatter = new BinaryFormatter();
stream = new FileStream("TaskPane.bin", FileMode.Create, FileAccess.Write,
FileShare.None);
TaskPane.TaskPaneSurrogate taskPaneSurrogate =
new TaskPane.TaskPaneSurrogate();
taskPaneSurrogate.Load(this.serializeTaskPane);
formatter.Serialize(stream, taskPaneSurrogate);
stream.Close();
// deserialize a TaskPane from a file
IFormatter formatter = new BinaryFormatter();
stream = new FileStream("TaskPane.bin", FileMode.Open, FileAccess.Read,
FileShare.Read);
TaskPane.TaskPaneSurrogate taskPaneSurrogate =
(TaskPane.TaskPaneSurrogate) formatter.Deserialize(stream);
TaskPane taskpane = taskPaneSurrogate.Save();
stream.Close();
// XML SERIALIZATION
// serialize a TaskPane to a file
XmlSerializer xml = new XmlSerializer(typeof(TaskPane.TaskPaneSurrogate));
StreamWriter writer = new StreamWriter("TaskPane.xml");
TaskPane.TaskPaneSurrogate taskPaneSurrogate =
new TaskPane.TaskPaneSurrogate();
taskPaneSurrogate.Load(this.serializeTaskPane);
xml.Serialize(writer, taskPaneSurrogate);
writer.Close();
// deserialize a TaskPane from a file
XmlSerializer xml = new XmlSerializer(typeof(TaskPane.TaskPaneSurrogate));
TextReader reader = new StreamReader("TaskPane.xml");
TaskPane.TaskPaneSurrogate taskPaneSurrogate =
(TaskPane.TaskPaneSurrogate) xml.Deserialize(reader);
TaskPane taskpane = taskPaneSurrogate.Save();
reader.Close();
Note: Controls in the Visual Styles and WM_PRINTUpdate: I sent a bug report to Microsoft about visual styles and Some XP themed controls (
Figure 12: TextBox border after In order to solve this problem, I immediately ran into another problem - How to find out if Visual Styles are applied (i.e., a manifest or ///
/// Checks whether Visual Styles are enabled
///
protected bool VisualStylesEnabled
{
get
{
OperatingSystem os = System.Environment.OSVersion;
// check if the OS is XP or higher
if (os.Platform == PlatformID.Win32NT &&
((os.Version.Major == 5 && os.Version.Minor >= 1) ||
os.Version.Major > 5))
{
// are themes enabled
if (UxTheme.IsThemeActive() && UxTheme.IsAppThemed())
{
DLLVERSIONINFO version = new DLLVERSIONINFO();
version.cbSize = Marshal.SizeOf(typeof(DLLVERSIONINFO));
// are we using Common Controls v6
if (DllGetVersion(ref version) == 0)
{
return (version.dwMajorVersion > 5);
}
}
}
return false;
}
}
///
/// Receives dynamic-link library (DLL)-specific version information.
/// It is used with the DllGetVersion function
///
[StructLayout(LayoutKind.Sequential)]
public struct DLLVERSIONINFO
{
public int cbSize;
public int dwMajorVersion;
public int dwMinorVersion;
public int dwBuildNumber;
public int dwPlatformID;
}
///
/// Implemented by many of the Microsoft Windows Shell dynamic-link libraries
/// (DLLs) to allow applications to obtain DLL-specific version information
///
[DllImport("Comctl32.dll")]
public static extern int DllGetVersion(ref DLLVERSIONINFO pdvi);
I then subclassed the offending controls and listened for ///
/// Processes Windows messages
///
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
// don't bother if visual styles aren't applied
if (!this.visualStylesEnabled)
{
return;
}
// WM_PRINT message?
if (m.Msg == (int) WindowMessageFlags.WM_PRINT)
{
// are we supposed to draw the nonclient area?
// (ie borders)
if ((m.LParam.ToInt32() & (int) WmPrintFlags.PRF_NONCLIENT) ==
(int) WmPrintFlags.PRF_NONCLIENT)
{
// open theme data
IntPtr hTheme = UxTheme.OpenThemeData(this.Handle,
UxTheme.WindowClasses.Edit);
if (hTheme != IntPtr.Zero)
{
// get the part and state needed
int partId = (int) UxTheme.Parts.Edit.EditText;
int stateId = (int) UxTheme.PartStates.EditText.Normal;
// rectangle to draw into
RECT rect = new RECT();
rect.right = this.Width;
rect.bottom = this.Height;
// clipping rectangle
RECT clipRect = new RECT();
// draw the left border
clipRect.left = rect.left;
clipRect.top = rect.top;
clipRect.right = rect.left + 2;
clipRect.bottom = rect.bottom;
UxTheme.DrawThemeBackground(hTheme, m.WParam, partId, stateId,
ref rect, ref clipRect);
// do the same for other borders
...
}
UxTheme.CloseThemeData(hTheme);
}
}
}
The subclassed controls ( Known Problems
History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||