1, 看看成果:
换肤前
换肤后
(说明: 这里仅仅借用的是"千千静听"中皮肤包中的图片, 本示例程序中的皮肤包文件格式以及 换肤方案均为作者原创)
2 如何实现:
2.1 皮肤配置文件:
在研究如何实现 换肤之前,应该仔细看看以下XML文件,它是皮肤配置文件,由它来组织皮肤包中的图片并告诉软件如何来绘制皮肤:
复制
保存
<?xml version="1.0" encoding="utf-8"?>
<PlayerSkin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<MainWindow>
<Image>player_skin.bmp</Image>
<Icon>
<Position>
<Left>1</Left>
<Top>1</Top>
<Right>17</Right>
<Bottom>17</Bottom>
</Position>
<IconImage>icon.ico</IconImage>
</Icon>
<Play>
<Position>
<Left>164</Left>
<Top>39</Top>
<Right>183</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>play.bmp</Iamge>
</Play>
<Pause>
<Position>
<Left>164</Left>
<Top>39</Top>
<Right>183</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>pause.bmp</Iamge>
</Pause>
<Stop>
<Position>
<Left>145</Left>
<Top>39</Top>
<Right>164</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>stop.bmp</Iamge>
</Stop>
<Next>
<Position>
<Left>202</Left>
<Top>39</Top>
<Right>221</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>next.bmp</Iamge>
</Next>
<Previous>
<Position>
<Left>183</Left>
<Top>39</Top>
<Right>202</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>prev.bmp</Iamge>
</Previous>
<Mute>
<Position>
<Left>249</Left>
<Top>39</Top>
<Right>268</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>mute.bmp</Iamge>
</Mute>
<Open>
<Position>
<Left>221</Left>
<Top>39</Top>
<Right>240</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>open.bmp</Iamge>
</Open>
<Eq>
<Position>
<Left>152</Left>
<Top>92</Top>
<Right>188</Right>
<Bottom>114</Bottom>
</Position>
<Iamge>equalizer.bmp</Iamge>
</Eq>
<PalyList>
<Position>
<Left>188</Left>
<Top>92</Top>
<Right>224</Right>
<Bottom>114</Bottom>
</Position>
<Iamge>playlist.bmp</Iamge>
</PalyList>
<Lyric>
<Position>
<Left>224</Left>
<Top>92</Top>
<Right>260</Right>
<Bottom>114</Bottom>
</Position>
<Iamge>lyric.bmp</Iamge>
</Lyric>
<Visual>
<Position>
<Left>18</Left>
<Top>24</Top>
<Right>124</Right>
<Bottom>84</Bottom>
</Position>
</Visual>
<MiniMize>
<Position>
<Left>240</Left>
<Top>3</Top>
<Right>247</Right>
<Bottom>10</Bottom>
</Position>
<Iamge>minimize.bmp</Iamge>
</MiniMize>
<MiniMode>
<Position>
<Left>253</Left>
<Top>3</Top>
<Right>260</Right>
<Bottom>10</Bottom>
</Position>
<Iamge>minimode.bmp</Iamge>
</MiniMode>
<Exit>
<Position>
<Left>266</Left>
<Top>3</Top>
<Right>273</Right>
<Bottom>10</Bottom>
</Position>
<Iamge>close.bmp</Iamge>
</Exit>
<Progress>
<Position>
<Left>145</Left>
<Top>77</Top>
<Right>267</Right>
<Bottom>90</Bottom>
</Position>
<BarImage></BarImage>
<ThumbImage>progress_thumb.bmp</ThumbImage>
<FillImage>progress_fill.bmp</FillImage>
</Progress>
<Volume>
<Position>
<Left>274</Left>
<Top>20</Top>
<Right>283</Right>
<Bottom>84</Bottom>
</Position>
<BarImage></BarImage>
<ThumbImage></ThumbImage>
<FillImage>volume_fill.bmp</FillImage>
<Vertical>true</Vertical>
</Volume>
<Info>
<Position>
<Left>147</Left>
<Top>20</Top>
<Right>268</Right>
<Bottom>31</Bottom>
</Position>
<Color>#ff9933</Color>
<Font>simsong</Font>
<FontSize>9</FontSize>
<Align>MiddleLeft</Align>
</Info>
<Led>
<Position>
<Left>175</Left>
<Top>63</Top>
<Right>235</Right>
<Bottom>74</Bottom>
</Position>
<Iamge>number.bmp</Iamge>
<Align>MiddleRight</Align>
</Led>
<Stereo>
<Position>
<Left>13</Left>
<Top>98</Top>
<Right>55</Right>
<Bottom>109</Bottom>
</Position>
<Color>#ff9933</Color>
<Font>simsong</Font>
<FontSize>9</FontSize>
<Align>MiddleLeft</Align>
</Stereo>
<Status>
<Position>
<Left>59</Left>
<Top>98</Top>
<Right>129</Right>
<Bottom>109</Bottom>
</Position>
<Color>#ff9933</Color>
<Font>simsong</Font>
<FontSize>9</FontSize>
<Align>MiddleLeft</Align>
</Status>
</MainWindow>
<Name>Orange</Name>
<TransparentColor>#ff00ff</TransparentColor>
</PlayerSkin>
<?xml version="1.0" encoding="utf-8"?> <PlayerSkin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <MainWindow> <Image>player_skin.bmp</Image> <Icon> <Position> <Left>1</Left> <Top>1</Top> <Right>17</Right> <Bottom>17</Bottom> </Position> <IconImage>icon.ico</IconImage> </Icon> <Play> <Position> <Left>164</Left> <Top>39</Top> <Right>183</Right> <Bottom>58</Bottom> </Position> <Iamge>play.bmp</Iamge> </Play> <Pause> <Position> <Left>164</Left> <Top>39</Top> <Right>183</Right> <Bottom>58</Bottom> </Position> <Iamge>pause.bmp</Iamge> </Pause> <Stop> <Position> <Left>145</Left> <Top>39</Top> <Right>164</Right> <Bottom>58</Bottom> </Position> <Iamge>stop.bmp</Iamge> </Stop> <Next> <Position> <Left>202</Left> <Top>39</Top> <Right>221</Right> <Bottom>58</Bottom> </Position> <Iamge>next.bmp</Iamge> </Next> <Previous> <Position> <Left>183</Left> <Top>39</Top> <Right>202</Right> <Bottom>58</Bottom> </Position> <Iamge>prev.bmp</Iamge> </Previous> <Mute> <Position> <Left>249</Left> <Top>39</Top> <Right>268</Right> <Bottom>58</Bottom> </Position> <Iamge>mute.bmp</Iamge> </Mute> <Open> <Position> <Left>221</Left> <Top>39</Top> <Right>240</Right> <Bottom>58</Bottom> </Position> <Iamge>open.bmp</Iamge> </Open> <Eq> <Position> <Left>152</Left> <Top>92</Top> <Right>188</Right> <Bottom>114</Bottom> </Position> <Iamge>equalizer.bmp</Iamge> </Eq> <PalyList> <Position> <Left>188</Left> <Top>92</Top> <Right>224</Right> <Bottom>114</Bottom> </Position> <Iamge>playlist.bmp</Iamge> </PalyList> <Lyric> <Position> <Left>224</Left> <Top>92</Top> <Right>260</Right> <Bottom>114</Bottom> </Position> <Iamge>lyric.bmp</Iamge> </Lyric> <Visual> <Position> <Left>18</Left> <Top>24</Top> <Right>124</Right> <Bottom>84</Bottom> </Position> </Visual> <MiniMize> <Position> <Left>240</Left> <Top>3</Top> <Right>247</Right> <Bottom>10</Bottom> </Position> <Iamge>minimize.bmp</Iamge> </MiniMize> <MiniMode> <Position> <Left>253</Left> <Top>3</Top> <Right>260</Right> <Bottom>10</Bottom> </Position> <Iamge>minimode.bmp</Iamge> </MiniMode> <Exit> <Position> <Left>266</Left> <Top>3</Top> <Right>273</Right> <Bottom>10</Bottom> </Position> <Iamge>close.bmp</Iamge> </Exit> <Progress> <Position> <Left>145</Left> <Top>77</Top> <Right>267</Right> <Bottom>90</Bottom> </Position> <BarImage></BarImage> <ThumbImage>progress_thumb.bmp</ThumbImage> <FillImage>progress_fill.bmp</FillImage> </Progress> <Volume> <Position> <Left>274</Left> <Top>20</Top> <Right>283</Right> <Bottom>84</Bottom> </Position> <BarImage></BarImage> <ThumbImage></ThumbImage> <FillImage>volume_fill.bmp</FillImage> <Vertical>true</Vertical> </Volume> <Info> <Position> <Left>147</Left> <Top>20</Top> <Right>268</Right> <Bottom>31</Bottom> </Position> <Color>#ff9933</Color> <Font>simsong</Font> <FontSize>9</FontSize> <Align>MiddleLeft</Align> </Info> <Led> <Position> <Left>175</Left> <Top>63</Top> <Right>235</Right> <Bottom>74</Bottom> </Position> <Iamge>number.bmp</Iamge> <Align>MiddleRight</Align> </Led> <Stereo> <Position> <Left>13</Left> <Top>98</Top> <Right>55</Right> <Bottom>109</Bottom> </Position> <Color>#ff9933</Color> <Font>simsong</Font> <FontSize>9</FontSize> <Align>MiddleLeft</Align> </Stereo> <Status> <Position> <Left>59</Left> <Top>98</Top> <Right>129</Right> <Bottom>109</Bottom> </Position> <Color>#ff9933</Color> <Font>simsong</Font> <FontSize>9</FontSize> <Align>MiddleLeft</Align> </Status> </MainWindow> <Name>Orange</Name> <TransparentColor>#ff00ff</TransparentColor> </PlayerSkin>
很明显地,该配置文件中规定了皮肤中的各个元素的位置大小等相关信息. 比如:
复制
保存
<Play>
<Position>
<Left>164</Left>
<Top>39</Top>
<Right>183</Right>
<Bottom>58</Bottom>
</Position>
<Iamge>play.bmp</Iamge>
</Play>
<Play> <Position> <Left>164</Left> <Top>39</Top> <Right>183</Right> <Bottom>58</Bottom> </Position> <Iamge>play.bmp</Iamge> </Play>
规定的"播放"按钮在主窗口中的位置以及其对应的图片.
很简单地,我们只要解析该XML文件并将其对应的图片绘制在指定的位置就可以了(当然此时还只是视觉上的,因为按钮还有事件等,稍后讨论)
2.2 如何组织窗口上的元素
"窗口上的元素"指的是窗口上的"按钮","滚动条"等. 注意:它们不是"Button" "ScrollBar"等控件,它们只是在指定位置上的图片,并响应相应事件)
为更好地组织各个元素,我们设计了一个"皮肤元素接口",让所有元素都实现该接口:
复制
保存
public interface ISkinElement
{
string Name
{
get;
}
Position Position
{
get;
set;
}
ElementStatus Status
{
get;
set;
}
void Paint(Graphics g);
event SkinElementStatusChangedHandler SkinElementStatusChanged;
}
其中Name表示该元素的名称.
Position规定了该元素的大小和位置,它是这样一个结构:
复制
保存
/// <summary>
/// 位置信息,由左上角X、Y坐标和右下角X、Y坐标组成
/// </summary>
public struct Position
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public Position(int left, int top, int right, int bottom)
{
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
}
}
Status表示该元素的当前状态,它是这样一个枚举:
复制
保存
/// <summary>
/// 元素状态
/// </summary>
public enum ElementStatus
{
Normal,
MouseHover,
MouseDown,
Disabled
}
void Paint(Graphics g)是该元素的绘制函数
event SkinElementStatusChangedHandler SkinElementStatusChanged;是该元素状态发生改变时所引发的事件.
然后,比如"播放"按钮,就可以是如下的一个类:
复制
保存
/// <summary>
/// 播放元素(播放按钮)
/// </summary>
public class PlayElement : ISkinElement
{
private Position position;
private string image = string.Empty;
[NonSerialized]
private ElementStatus status;
#region SkinElement 成员
/// <summary>
/// 获取该元素的名称
/// </summary>
public string Name
{
get { return "PlayElement"; }
}
/// <summary>
/// 获取或设置该元素的位置
/// </summary>
public Position Position
{
get { return this.position; }
set { this.position = value; }
}
/// <summary>
/// 绘制该元素
/// </summary>
public void Paint(Graphics g)
{
SkinPainter.PaintSkinElement(g, this.position, this.image, this.status);
}
/// <summary>
/// 获取或设置该对象的状态
/// </summary>
public ElementStatus Status
{
get { return this.status; }
set
{
if (this.status != value)
{
this.status = value;
this.OnSkinElementStatusChanged(new SkinElementStatusChangedArges(value));
}
}
}
/// <summary>
/// 当元素状态改变时发生
/// </summary>
public event SkinElementStatusChangedHandler SkinElementStatusChanged;
#endregion
protected void OnSkinElementStatusChanged(SkinElementStatusChangedArges arg)
{
if (this.SkinElementStatusChanged != null)
{
this.SkinElementStatusChanged(this, arg);
}
}
/// <summary>
/// 获取或设置该对象对应的图片文件(相对于皮肤文件夹的相对路径)
/// </summary>
public string Iamge
{
get { return this.image; }
set { this.image = value; }
}
}
其它元素同理.
2.3 如何绘制元素
以按钮为例:
一张按钮图片由四部分组成 , 它分别代表按钮的不同状态(Normal,MouseEnter,MouseDown, Disable), 我们只需要根据按钮的当前状态切取图片的不同部分并绘制在指定位置上便可.比如:
复制
保存
/// <summary>
/// 绘制普通的类似于按钮的皮肤元素。
/// 进度条、音量控制等不应该采用此函数
/// </summary>
/// <param name="g">用其进行绘制</param>
/// <param name="pos">绘制的位置</param>
/// <param name="imgPath">绘制的图片的路径(相对路径,在此之前应该确定SkinRootDir属性已经被设置)</param>
/// <param name="status">皮肤元素的当前状态</param>
public static void PaintSkinElement(Graphics g, Position pos, string imgPath, ElementStatus status)
{
ImageAttributes imgAttributes = new ImageAttributes();
imgAttributes.SetColorKey(transparentKey, transparentKey);
try
{
Image img = Image.FromFile(SkinRootDir + System.IO.Path.DirectorySeparatorChar + imgPath);
Rectangle destRect = new Rectangle(pos.Left, pos.Top, pos.Right - pos.Left, pos.Bottom - pos.Top);
int x = 0;
int y = 0;
int width = img.Width / 4;
int height = img.Height;
switch (status)
{
case ElementStatus.Normal:
x = 0;
break;
case ElementStatus.MouseHover:
x = width;
break;
case ElementStatus.MouseDown:
x = 2 * width;
break;
case ElementStatus.Disabled:
x = 3 * width;
break;
default:
break;
}
g.DrawImage(img, destRect, x, y, width, height, GraphicsUnit.Pixel, imgAttributes);
}
catch
{
}
}
2.4 如何响应键盘鼠标等事件
以鼠标事件为例:
其实皮肤上的元素并没有这些事件,我们只是当用户点击主窗口时,根据鼠标点击的位置来确定点击在了哪个元素指上,并引发该元素所对应的事件.
查找鼠标点击的元素:
复制
保存
/// <summary>
/// 确定在指定的皮肤元素集合中,指定的点包含在哪个元素中.
/// 如果同时包含在多个元素中,则以最内层的那个为准.
/// <para>注意:由于元素之间没有Z轴层次关系,所以不应该让两个元素处在相交却不包含的关系中</para>
/// </summary>
/// <param name="elementList">元素集合,在它们中间查找</param>
/// <param name="loc">要进行判断的点</param>
/// <returns>如果不包含在指定的任一元素中,则返回null,否则返回包含该点的元素</returns>
public static ISkinElement FindSkinElementFormPosition(List<ISkinElement> elementList, Point loc)
{
Rectangle rect = new Rectangle();
Rectangle lastRect = Rectangle.Empty;
ISkinElement res = null;
foreach (ISkinElement element in elementList)
{
rect.X = element.Position.Left;
rect.Y = element.Position.Top;
rect.Width = element.Position.Right - element.Position.Left;
rect.Height = element.Position.Bottom - element.Position.Top;
if (rect.Contains(loc))
{
if (lastRect == Rectangle.Empty || lastRect.Contains(rect))
{
lastRect = rect;
res = element;
}
}
}
return res;
}
比如,我们鼠标点击落在"关闭"元素内时,将关闭窗口:
复制
保存
private void FormMain_MouseClick(object sender, MouseEventArgs e)
{
ISkinElement element =
Helper.FindSkinElementFormPosition(this.skinMainFormElementList, e.Location);
if (element != null)
{
switch (element.Name)
{
case "ExitElement":
this.Close();
break;
default:
break;
}
}
}
2.5 局部区域更新
比如鼠标移动到"播放"按钮上时,其状态将转换为"MouseEnter",将重新绘制该按钮以响应鼠标.此时只需要更新该按钮所对应的区域便可:
复制
保存
private void FormMain_MouseMove(object sender, MouseEventArgs e)
{
ISkinElement element =
Helper.FindSkinElementFormPosition(this.skinMainFormElementList, e.Location);
if (element != null &&
element.Status != ElementStatus.MouseHover &&
element.Status != ElementStatus.Disabled)
{
element.Status = ElementStatus.MouseHover;
this.UpdateSkinElement(element);
}
}
其中UpdateSkinElement(ISkinElement element):
复制
保存
/// <summary>
/// 更新指定皮肤元素(重新绘制皮肤的指定区域)
/// </summary>
/// <param name="element">要被重绘的元素</param>
private void UpdateSkinElement(ISkinElement element)
{
Rectangle updateRect = new Rectangle(
element.Position.Left,
element.Position.Top,
element.Position.Right - element.Position.Left,
element.Position.Bottom - element.Position.Top);
this.Invalidate(updateRect);
this.Update();
}