不仅仅是图片列表控件,简洁、3D、iTunes风格






4.73/5 (19投票s)
该图片列表控件以3D方式显示所有图片,选中时会为项目添加动画效果,并允许控制项目的大小、透明度、位置和动画速度。
引言
灵感来自于iTunes的专辑列表控件。这款图片列表控件可以让你以一种全新的3D方式展示图片。告别乏味的逐一显示图片,取而代之的是炫酷的动画效果。你可以控制未选中项目的缩放级别,以及阴影的高度、每个未选中项目的透明度、它们的位置和动画速度。
这是一款非常有用的控件,用于在GUI丰富的应用程序中显示图片。该控件派生自CListCtrl
类,因此可以扩展以支持传统的列表控件视图或炫酷的3D动画视图。
背景
正如我之前提到的,灵感来自于iTunes的专辑控件。在周末享受音乐时,我一直在想,如何才能拥有一个像iTunes那样具有流畅、简洁动画的图片展示控件。于是我接受了挑战,发现只需要大约10个代数方程和对GDI+的巧妙运用就能实现。
该控件加载全尺寸图片,在演示中,它使用了高达800x600的图片。通过高效地使用GDI+,你可以流畅地进行动画、缩放和控制Alpha值,而不会出现任何闪烁。
本文档对于希望了解更多关于使用GDI+绘图的开发人员非常有帮助。尽管GDI+几乎包含了所需的一切,但无法像iTunes那样绘制图片。它允许对图片进行剪切,但不支持使图片的上下两边具有不同的高度。我并没有真正尝试使用矩阵变换,但我认为使用它们可以更好地实现图片的动画和定位。
Using the Code
要在你的项目中 S 用该控件,请添加AlubumCtrl.h和AlbumCtrl.cpp,在你的窗体/对话框上添加一个列表控件,为此列表控件创建一个类型为CAlbumCtrl
的成员变量,并在你的OnInitDialog
或任何你想向列表控件添加新项目的其他地方添加以下代码。
m_ctlAlbum.AddItem(L"images\\Water lilies.jpg");
m_ctlAlbum.AddItem(L"images\\Sunset.jpg");
m_ctlAlbum.AddItem(L"images\\Winter.jpg");
m_ctlAlbum.AddItem(L"images\\Blue hills.jpg");
m_ctlAlbum.SetCurrentItem(2);
m_sldAlpha.SetRange(0,100);
m_sldAnim.SetRange(0,100);
m_sldShadow.SetRange(0,100);
m_sldZoom.SetRange(0,100);
m_sldElevation.SetRange(-300,100);
m_sldElevation.SetPos(m_ctlAlbum.GetItemElevation());
m_sldAlpha.SetPos(m_ctlAlbum.GetItemAlpha());
m_sldAnim.SetPos(m_ctlAlbum.GetAnimSpeed());
m_sldShadow.SetPos(m_ctlAlbum.GetItemShadow());
m_sldZoom.SetPos(m_ctlAlbum.GetItemZoom());
计算项目位置
以下函数是放置控件最重要的函数,令人惊讶的是,代码量非常少。是的,在经过大约4轮的代数和代码微调后,才有了现在的样子。 :)
RectF CAlbumCtrl::CalcItemRect(const int n, const RectF rcBase, bool bLeft)
{
float r = m_fRatio;
float p = pow(r,n);
int h = rcBase.Height;
int w = rcBase.Width;
int y = 0;
int x = 0;
for(int j=1;j<=n;j++)
y += (m_nItemY*h*pow(r,j))/100;
if( bLeft )
{
int x = 0;
for(int j=2;j<=n+1;j++)
x += w*pow(r,j);
return RectF(rcBase.X-x, rcBase.Y-y, w*p, h*p);
}
x = rcBase.X + w;
for(int j=1;j<n;j++)
x += w*pow(r,j);
for(int j=1;j<n+1;j++)
x -= (1-r)*w*pow(r,j);
return RectF(x, rcBase.Y-y, w*p, h*p);
}
绘制项目
这是控件中第二重要的函数。它获取项目的图片,计算阴影宽度,应用Alpha值到图片和逐渐减弱的阴影,如果需要,还会在选中的图片上放置文本。
void CAlbumCtrl::DrawItem
(const int nItem, RectF rc, Graphics &grf, float fAlpha, bool bDrawText)
{
// centre image
// draw the items only if they are inside the visible area
// to keep repainting fast
if( nItem < 0 || nItem > m_vecItems.size()-1 )
return;
Bitmap bitmap(m_vecItems[nItem].wszItem.c_str());
CRect r;
GetClientRect(&r);
RectF rcWnd(r.left, r.top, r.Width(), r.Height());
if(!rcWnd.IntersectsWith(rc))
return;
Bitmap bmp(rc.Width, rc.Height, PixelFormat32bppARGB );
Graphics graphic(&bmp);
ImageAttributes imgAttrb;
RectF rcDraw(0, 0, rc.Width, rc.Height);
graphic.DrawImage(&bitmap, rcDraw, 0, 0, bitmap.GetWidth(),
bitmap.GetHeight(), UnitPixel, &imgAttrb);
graphic.DrawRectangle(&Pen(Color(80,80,80), 2),rcDraw);
ColorMatrix bmpAlpha = {
1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, fAlpha, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 1.0f
};
imgAttrb.SetColorMatrix(&bmpAlpha);
grf.DrawImage(&bmp, rc, 0, 0, rc.Width, rc.Height, UnitPixel, &imgAttrb);
int iHeight = bmp.GetHeight();
int iWidth = bmp.GetWidth();
// reflection percent
if( !m_nItemShadow )
m_nItemShadow = 1;
float nRef = 100.0/m_nItemShadow;
RectF rcRef = RectF(rc.X, rc.Y+rc.Height, rc.Width, rc.Height/nRef);
Bitmap bmpRef(rcDraw.Width, rcDraw.Height/nRef, PixelFormat32bppARGB );
Color color, colorTemp;
for(UINT iRow = iHeight; iRow > iHeight-iHeight/nRef; iRow--)
{
for(UINT iColumn = 0; iColumn < iWidth; iColumn++)
{
// decrease the alpha by 1 value for each line in bottom margin
double dAlpha = (iHeight/nRef-(iHeight-iRow))*255/(iHeight/nRef);
bmp.GetPixel(iColumn, iRow, &color);
colorTemp.SetValue(color.MakeARGB(
//(BYTE)(5*(iHeight/nRef-(iHeight-iRow))),
(BYTE)dAlpha,
color.GetRed(),
color.GetGreen(),
color.GetBlue()));
bmpRef.SetPixel(iColumn, iHeight-iRow, colorTemp);
}
}
grf.DrawImage(&bmpRef, rcRef, 0, 0, rcRef.Width, rcRef.Height,
UnitPixel, &imgAttrb);
if( bDrawText )
{
//wstring wszText = m_vecItems[nItem].wszItem;
//Font myFont(L"Tahoma",12,FontStyleRegular,UnitPixel);
//
//RectF rcText;
//grf.MeasureString(wszText.c_str(), wszText.length(), &myFont,
// rcRef, &rcText);
//grf.DrawString(wszText.c_str(),wszText.length(), &myFont,
// PointF(rcRef.X+rcRef.Width-rcText.Width-14,
// rcRef.Y+(rcRef.Height-rcText.Height)/2-rcText.Height),
// &SolidBrush(Color(255,255,255)));
//wchar_t temp[10];
//wszText = _itow(nItem+1, temp, 10);
//grf.DrawString(wszText.c_str(),wszText.length(), &myFont,
// PointF(rcRef.X+2, rcRef.Y+(rcRef.Height-rcText.Height)/2-rcText.Height),
// &SolidBrush(Color(255,255,255)));
//grf.DrawString(wszText.c_str(),wszText.length(), &myFont,
// PointF(rcRef.X, rcRef.Y), &SolidBrush(Color(255,255,255)));
}
}
幕后涉及的数学原理
控件中最有趣的部分是用于定位和动画化项目的数学原理。其中,在中心选中项目左右两侧的项目定位/动画化方面,涉及不同的代数运算。
在控件中定位项目
计算任何项目的宽度
Wn = W(r/100)n
计算任何项目的高度
Hn = H(r/100)n
计算任何项目的Y坐标
Yn = Y - | n ∑ i = 1 | H/e(r/100) |
计算选中项目左侧项目的X坐标
Xn = X - | n+1 ∑ i = 2 | W(r/100) |
计算选中项目右侧项目的X坐标
Xn = X + W + | n-1 ∑ i = 1 | W(r/100)i - | n+1 ∑ i = 1 | W(1-r/100)(r/100) |
其中
H
= 选中项目的高度W
= 选中项目的宽度X
= 选中项目的X坐标Y
= 选中项目的Y坐标r
= 当前缩放级别(%)e
= 当前Y轴的抬升值
在控件中动画化项目
向左动画化项目
左侧动画化项目
∆X = c(Xn+1-Xn)/l
Xn = Xn + ∆X
∆Y = c(Yn+1-Yn)/l
Yn = Yn + ∆Y
∆W = c(Wn+1-Wn)/l
Wn = Wn + ∆W
∆H = c(Hn+1-Hn)/l
Hn = Hn + ∆H
右侧动画化项目
∆X = c(Xn+1-Xn)/l
Xn-1 = Xn + ∆X
∆Y = c(Yn+1-Yn)/l
Yn-1 = Yn - ∆Y
∆W = c(Wn+1-Wn)/l
Wn = Wn - ∆W
∆H = c(Hn+1-Hn)/l
Hn = Hn - ∆H
向右动画化项目
左侧动画化项目
∆X = c(Xn+1-Xn)/l
Xn = Xn + ∆X
∆Y = c(Yn+1-Yn)/l
Yn = Yn + ∆Y
∆W = c(Wn+1-Wn)/l
Wn = Wn + ∆W
∆H = c(Hn+1-Hn)/l
Hn = Hn + ∆H
右侧动画化项目
∆X = c(Xn+1-Xn)/l
Xn = Xn + ∆X
∆Y = c(Yn+1-Yn)/l
Yn = Yn - ∆Y
∆W = c(Wn-1-Wn)/l
Wn = Wn - ∆W
∆H = c(Hn-1-Hn)/l
Hn = Hn - ∆H
其中
- c = 当前动画步长
- l = 总动画步数
未来功能
我计划添加视图切换按钮,以便用户可以在图标视图/列表视图/报表视图/专辑视图之间来回切换。
历史
- 2007年10月29日:初始版本