图像查看器 UserControl






4.97/5 (106投票s)
本文介绍了我编写的一个用户控件。与在窗体上显示图像的 PictureBox 和其他方法不同,这个控件提供了一种完全不同的方法。

引言
ImageViewer UserControl
是我为了解决我在窗体上显示图像时遇到的一个问题而创建的。我希望能够缩放、旋转我的图像,并且最重要的是,不必为了适应我的窗体而缩小图像。
我四处寻找并找到了诸如将 PictureBox
拖放到面板中之类的解决方案,这可能不错,但也有其问题。通过本文,我想与那些可能面临同样问题的人分享我的工作。
用户控件的属性
AllowDrop |
bool | 一个用于启用或禁用控件上拖放功能的属性。 |
BackgroundColor |
Color | 一个用于获取或设置图片面板背景颜色的属性。 |
GifAnimation |
bool | 一个用于启用或禁用 *.gif 文件动画的属性。 |
GifFPS |
double | 一个用于调整 *.gif 文件动画的每秒帧数的属性。范围在 1 到 30 FPS 之间。 |
Image |
Bitmap | 一个用于获取或设置控件显示的图像的属性。 |
ImagePath |
字符串 | 一个用于设置图像的物理路径的属性(C:\Image.jpg)。 |
MenuColor |
Color | 一个用于调整整个菜单使用的颜色的属性。 |
MenuPanelColor |
Color | 一个用于仅调整菜单面板使用的颜色的属性。 |
OpenButton |
bool | 一个用于启用或禁用 UserControl 菜单上“打开”按钮的属性。 |
NavigationPanelColor |
Color | 一个用于仅调整导航面板使用的颜色的属性。 |
PanelWidth |
int | 一个返回图片面板宽度的属性。 |
PanelHeight |
int | 一个返回图片面板高度的属性。 |
PreviewButton |
bool | 一个用于启用或禁用 UserControl 菜单上“预览”切换按钮的属性。 |
PreviewPanelColor |
Color | 一个用于仅调整预览面板使用的颜色的属性。 |
PreviewText |
字符串 | 一个用于编辑预览标签文本的属性。 |
滚动条 |
bool | 一个用于启用或禁用滚动条的属性。 |
TextColor |
Color | 一个用于调整所有标签使用的颜色的属性。 |
NavigationTextColor |
Color | 一个用于调整导航标签使用的颜色的属性。 |
PreviewTextColor |
Color | 一个用于调整预览标签使用的颜色的属性。 |
旋转 |
int | 一个用于获取或设置图像旋转角度(0、90、180 或 270 度)的属性。 |
ShowPreview |
bool | 一个用于启用或禁用预览面板的属性。 |
缩放 |
int | 一个用于获取缩放百分比的属性。 |
OriginalSize |
大小 | 一个用于获取图像原始尺寸的属性。 |
CurrentSize |
大小 | 一个用于获取图像当前尺寸的属性。 |
用户控件的事件
AfterRotation |
图像旋转后触发的事件。
|
AfterZoom
图像放大或缩小时触发的事件。可用属性 | ||
缩放 |
int |
一个获取缩放百分比的属性。 |
InOut |
KpZoom |
一个返回是 ZoomIn 操作还是 ZoomOut 操作的属性。 |
Using the Code
与任何 UserControl
一样,将其拖到窗体上即可。要将控件添加到工具箱,请执行以下步骤:
- 步骤 1:右键单击工具箱,然后单击“选择项...”
- 步骤 2:在“.NET Framework 组件”中,单击“浏览”按钮。
- 步骤 3:浏览到解压后的文件夹,然后选择“KP-ImageViewerV2.dll”。
- 步骤 4:确保选中
KpImageViewer
,然后单击“确定”。
ImageViewer
具有内置的“打开图像”按钮。但是,如果这不是您想要的,则可以通过编程方式设置图像,并通过将 OpenButton
属性设置为 false
来禁用“打开”按钮。
private void Form1_Load(object sender, EventArgs e)
{
kpImageViewer.OpenButton = false;
kpImageViewer.Image = new Bitmap(@"C:\chuckwallpaper.jpg");
}
此外,还有 3 种旋转功能可供使用。非常直接。
private void Form1_Load(object sender, EventArgs e)
{
kpImageViewer.Rotate90(); // Rotates the Image 90 degrees clockwise
kpImageViewer.Rotate180(); // Rotates the Image 180 degrees clockwise
kpImageViewer.Rotate270(); // Rotates the Image 270 degrees clockwise
}
用户控件代码
KpImageViewer
类继承自 System.Windows.Forms.UserControl
类。该类使用 2 个单独的类和一个额外的 UserControl
。DrawEngine
和 DrawObject
是使用的类,而 UserControl
是一个 DoubleBufferedPanel
。
public class PanelDoubleBuffered : System.Windows.Forms.Panel
{
public PanelDoubleBuffered()
{
this.DoubleBuffered = true;
this.UpdateStyles();
}
}
public partial class KpImageViewer : UserControl
{
private KP_DrawEngine drawEngine;
private KP_DrawObject drawing;
...
}
DrawEngine
负责在内存中存储一个与面板精确大小相同的位图。它将用于在内存中渲染图像并将其绘制到面板上。DrawEngine
在调整大小时会重新创建内存位图,以使高度和宽度与面板相等。
public void InitControl()
{
drawEngine.CreateDoubleBuffer(pbFull.CreateGraphics(), pbFull.Width, pbFull.Height);
}
private void KP_ImageViewerV2_Resize(object sender, EventArgs e)
{
InitControl();
drawing.AvoidOutOfScreen();
UpdatePanels(true);
}
DrawObject
包含 Viewer
的所有实际功能。它负责在内存中存储原始图像、缩放、旋转、拖动和跳转到原点(在预览面板上单击的位置)。这些功能由 KpImageViewer
类中触发的事件调用。例如,请看鼠标功能的片段。这些功能负责图像的拖动和选择。
private void pbFull_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
// Left Shift or Right Shift pressed? Or is select mode one?
if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
{
// Fancy cursor
pbFull.Cursor = Cursors.Cross;
shiftSelecting = true;
// Initial selection
ptSelectionStart.X = e.X;
ptSelectionStart.Y = e.Y;
// No selection end
ptSelectionEnd.X = -1;
ptSelectionEnd.Y = -1;
}
else
{
// Start dragging
drawing.BeginDrag(new Point(e.X, e.Y));
// Fancy cursor
if (grabCursor != null)
{
pbFull.Cursor = grabCursor;
}
}
}
}
private void pbFull_MouseUp(object sender, MouseEventArgs e)
{
// Am i dragging or selecting?
if (shiftSelecting == true)
{
// Calculate my selection rectangle
Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);
// Clear the selection rectangle
ptSelectionEnd.X = -1;
ptSelectionEnd.Y = -1;
ptSelectionStart.X = -1;
ptSelectionStart.Y = -1;
// Stop selecting
shiftSelecting = false;
// Position of the panel to the screen
Point ptPbFull = PointToScreen(pbFull.Location);
// Zoom to my selection
drawing.ZoomToSelection(rect, ptPbFull);
// Refresh my screen & update my preview panel
pbFull.Refresh();
UpdatePanels(true);
}
else
{
// Stop dragging and update my panels
drawing.EndDrag();
UpdatePanels(true);
// Fancy cursor
if (dragCursor != null)
{
pbFull.Cursor = dragCursor;
}
}
}
private void pbFull_MouseMove(object sender, MouseEventArgs e)
{
// Am I dragging or selecting?
if (shiftSelecting == true)
{
// Keep selecting
ptSelectionEnd.X = e.X;
ptSelectionEnd.Y = e.Y;
Rectangle pbFullRect = new Rectangle(0, 0, pbFull.Width - 1, pbFull.Height - 1);
// Am I still selecting within my panel?
if (pbFullRect.Contains(new Point(e.X, e.Y)))
{
// If so, draw my Rubber Band Rectangle!
Rectangle rect = CalculateReversibleRectangle(ptSelectionStart, ptSelectionEnd);
DrawReversibleRectangle(rect);
}
}
else
{
// Keep dragging
drawing.Drag(new Point(e.X, e.Y));
if (drawing.IsDragging)
{
UpdatePanels(false);
}
else
{
// I'm not dragging OR selecting
// Make sure if left or right shift is pressed to change cursor
if (this.IsKeyPressed(0xA0) || this.IsKeyPressed(0xA1) || selectMode == true)
{
// Fancy Cursor
if (pbFull.Cursor != Cursors.Cross)
{
pbFull.Cursor = Cursors.Cross;
}
}
else
{
// Fancy Cursor
if (pbFull.Cursor != dragCursor)
{
pbFull.Cursor = dragCursor;
}
}
}
}
}
函数: AvoidOutOfScreen()
用于防止图像浮出面板外部。它被编程为确保图像永远不会离开左上角(X: 0, Y: 0)。
我在这里遇到的主要问题是,一旦您拖动图像,就不希望 X 或 Y 坐标高于零。这本身不是问题,但当您查看 boundingBox.Left
和 boundingBox.Top
时,它就会出现。这些值将是负数,而 boundingBox.Width
和 boundingBox.Height
将是正数。将这些值相加会导致值不正确,并使图像拖动不正确。
我需要一个函数来确保 X、Y 坐标永远不高于零,并且永远不低于(图像宽度 - PanelWidth
) - ((图像宽度 - PanelWidth
) * 2)。
对于一个 480x320 的查看器和一个 1024x768 的图像,您将得到以下公式:
(1024 - 480 - ((1024 - 480) * 2)) = -544
这意味着最小 X 值将是 -544,以避免右侧出现浮动图像。
这是另一个可视化的例子,说明它是如何工作的。此图像的尺寸为 512x384。(请注意,这只是一个矩形,实际图像并未显示在屏幕外)
您可以在此处看到最小 X 值将是 -234。如果低于该值,您将在面板的右侧看到空白区域。
public void AvoidOutOfScreen()
{
try
{
if (boundingRect.X >= 0)
{
boundingRect.X = 0;
}
else if ((boundingRect.X <= (boundingRect.Width - panelWidth) -
((boundingRect.Width - panelWidth) * 2)))
{
if ((boundingRect.Width - panelWidth) -
((boundingRect.Width - panelWidth) * 2) <= 0)
{
boundingRect.X = (boundingRect.Width - panelWidth) -
((boundingRect.Width - panelWidth) * 2);
}
else
{
boundingRect.X = 0;
}
}
if (boundingRect.Y >= 0)
{
boundingRect.Y = 0;
}
else if ((boundingRect.Y <= (boundingRect.Height - panelHeight) -
((boundingRect.Height - panelHeight) * 2)))
{
if((boundingRect.Height - panelHeight) -
((boundingRect.Height - panelHeight) * 2) <= 0)
{
boundingRect.Y = (boundingRect.Height - panelHeight) -
((boundingRect.Height - panelHeight) * 2);
}
else
{
boundingRect.Y = 0;
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
}
}
函数: ZoomToSelection()
是 1.2 版本中的一项新功能,用于放大选定区域。在这里,我们计算出适合我们传入的选择的缩放位置和缩放量。我们还将一个 Point
变量传递到面板的 X,Y 坐标 PointToScreen()
。
public void ZoomToSelection(Rectangle selection, Point ptPbFull)
{
int x = (selection.X - ptPbFull.X);
int y = (selection.Y - ptPbFull.Y);
int width = selection.Width;
int height = selection.Height;
// So, where did my selection start on the entire picture?
int selectedX = (int)((double)(((double)boundingRect.X -
((double)boundingRect.X * 2)) + (double)x) / zoom);
int selectedY = (int)((double)(((double)boundingRect.Y -
((double)boundingRect.Y * 2)) + (double)y) / zoom);
int selectedWidth = width;
int selectedHeight = height;
// The selection width on the scale of the Original size!
if (zoom < 1.0 || zoom > 1.0)
{
selectedWidth = Convert.ToInt32((double)width / zoom);
selectedHeight = Convert.ToInt32((double)height / zoom);
}
// What is the highest possible zoomrate?
double zoomX = ((double)panelWidth / (double)selectedWidth);
double zoomY = ((double)panelHeight / (double)selectedHeight);
double newZoom = Math.Min(zoomX, zoomY);
// Avoid Int32 crashes!
if (newZoom * 100 < Int32.MaxValue && newZoom * 100 > Int32.MinValue)
{
SetZoom(newZoom);
selectedWidth = (int)((double)selectedWidth * newZoom);
selectedHeight = (int)((double)selectedHeight * newZoom);
// Center the selected area
int offsetX = 0;
int offsetY = 0;
if (selectedWidth < panelWidth)
{
offsetX = (panelWidth - selectedWidth) / 2;
}
if (selectedHeight < panelHeight)
{
offsetY = (panelHeight - selectedHeight) / 2;
}
boundingRect.X = (int)((int)((double)selectedX * newZoom) -
((int)((double)selectedX * newZoom) * 2)) + offsetX;
boundingRect.Y = (int)((int)((double)selectedY * newZoom) -
((int)((double)selectedY * newZoom) * 2)) + offsetY;
AvoidOutOfScreen();
}
}
功能: 拖放!(1.2 版本新增) 现在已支持!当 AllowDrop
设置为 true
时,面板将接受文件拖放到其上。为此,我像下面这样重载了 UserControl
上现有的 AllowDrop
属性。
public override bool AllowDrop
{
get
{
return base.AllowDrop;
}
set
{
this.pbFull.AllowDrop = value;
base.AllowDrop = value;
}
}
没什么特别的,我只是需要确保面板具有与 UserControl
本身相同的 AllowDrop
值。至于实际的拖放代码:
private void pbFull_DragDrop(object sender, DragEventArgs e)
{
try
{
// Get The file(s) you dragged into an array.
// (We'll just pick the first image anyway)
string[] FileList = (string[])e.Data.GetData(DataFormats.FileDrop, false);
Image newBmp = null;
for (int f = 0; f < FileList.Length; f++)
{
// Make sure the file exists!
if (System.IO.File.Exists(FileList[f]))
{
string ext = (System.IO.Path.GetExtension(FileList[f])).ToLower();
// Checking the extensions to be Image formats
if (ext == ".jpg" || ext == ".jpeg" || ext == ".gif" ||
ext == ".wmf" || ext == ".emf" || ext == ".bmp" ||
ext == ".png" || ext == ".tif" || ext == ".tiff")
{
try
{
// Try to load it into a bitmap
newBmp = Bitmap.FromFile(FileList[f]);
this.Image = (Bitmap)newBmp;
// If succeeded stop the loop
break;
}
catch
{
// Not an image?
}
}
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
}
}
private void pbFull_DragEnter(object sender, DragEventArgs e)
{
try
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
// Drop the file
e.Effect = DragDropEffects.Copy;
}
else
{
// I'm not going to accept this unknown format!
e.Effect = DragDropEffects.None;
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("ImageViewer error: " + ex.ToString());
}
}
关注点
这是我第二次尝试创建具有这些功能的 ImageViewer
。我的第一次尝试是让一个 PictureBox
拖到一个 Panel
上。最初这似乎效果不错,但无法在较大的图像上提供一个真正有效的缩放系统(堆大小问题)。这个查看器可以无限缩放而无需使用额外的内存。
已知问题
- 如果控件没有焦点,滚动缩放将不起作用。(单击控件或图像足以重新获得焦点)。
- 打开动画 *.gif 文件时,预览图像不会动画(这是故意的!)。
- 不支持带有 JPEG 压缩的多页 tiff 图像。Microsoft GDI+ 未对此问题提供任何解决方案。
版本历史
版本 1.5.1: (2011 年 9 月 12 日)- 修复了
FitToScreen
函数返回不正确结果的问题- 从项目中移除了所有
static
代码(导致了此问题) - 添加了两个额外的属性
PanelWidth
和PanelHeight
- 从项目中移除了所有
版本 1.5.0: (2011 年 9 月 1 日)
- 重新设计了 GIF 动画
- 移除了
ImageAnimator
并实现了一个自定义锁定系统 - 现在可以通过
GifFPS
属性指定帧速率
- 移除了
- 修复了某些 *.gif 图像旋转 180 度后不再动画的问题
- 修复了将多页 tiff 图像拖放到控件上时发生的崩溃
- 新功能: 完全支持滚动条
版本 1.4.0: (2011 年 8 月 15 日)
- 重新上传了演示项目。演示中现在又可以进行拖放了。
版本 1.4.0: (2011 年 8 月 2 日) 完整的更改列表
- 实现了对动画 *.gif 文件支持
版本 1.3.5: (2010 年 6 月 21 日) 完整的更改列表
- 进一步修复了多页 TIFF 旋转问题
版本 1.3.4: (2010 年 6 月 19 日) 完整的更改列表
- 修复了多页 TIFF 旋转
- 修复了通过 UNC 路径打开图像
- 修复了多页菜单的位置
- 修复了打开非图像格式(例如 *.txt)时双重
Try
和Catch
版本 1.3.3: (2010 年 5 月 6 日) 完整的更改列表
- 添加了 EMF/WMF 图像支持(感谢 wsmwlh!)
- 拖放现在也接受 EMF/WMF
- 对多页检查进行了
Try
和Catch
(在 WMF 文件上崩溃) - 修复了打开多页 Tiff 图像后,在打开非图像格式(例如 *.txt)时发生的
NullReference
版本 1.3.2: (2010 年 5 月 5 日) 完整的更改列表
- 修复了导航面板位置的一些进一步问题
版本 1.3.1: (2010 年 5 月 5 日) 完整的更改列表
- 修复了隐藏预览面板时导航面板位置不当的问题(感谢 wsmwlh 的提醒!)
版本 1.3: (2010 年 5 月 5 日) 完整的更改列表
- 添加了多页 TIFF 支持(请求)
- 为多页导航添加了其他属性
- 清理了解决方案(移除了重复图像及其引用)
版本 1.2: (2010 年 4 月 26 日) 由于资源图像过多,重新上传了演示项目和源代码文件(尽管有效,但不太好看!)
版本 1.2: (2010 年 4 月 23 日) 完整的更改列表
- 添加了拖放功能(请求)
- 增加了放大选中区域的可能性(选择缩放)
- 添加了一个新按钮和快捷方式(Shift + 鼠标单击)以使用选择缩放
- 添加了单页 TIF 支持
- 修复了打开只读图像时的 ReadOnly bug
- 为大量代码添加了注释,使其更清晰
- 修复了几个小 bug
版本 1.1.1: (2010 年 4 月 14 日) 完整的更改列表
- 修复了隐藏预览面板后绘制时的微小 bug
- 修复了预览面板错误折叠的 bug
版本 1.1: (2010 年 4 月 14 日) 完整的更改列表
- 添加了额外的
Color
属性以进行单独的颜色更改- 背景颜色(图片面板)
- 预览标签颜色
- 菜单的单独颜色可能性
- 修复了
AvoidOutOfScreen()
处理宽图像时的 bug - 优化了预览图像的渲染
- 现在可以在预览面板内进行拖动
- 现在可以启用或禁用预览面板
- 用户控制预览面板的按钮
- 可以在代码中强制设置(参见属性:
ShowPreview
&PreviewButton
)
- 现在可以通过在
ComboBox
中输入特定数字并按 Enter 来进行缩放 - 添加了一些漂亮的“手”和“拖动”光标
版本 1.0: (2010 年 4 月 7 日) 首次公开发布版本
结论
创建这个控件对我来说是一次非常有趣的经历,我希望很多人都能在他们的项目中找到它是一个有用的控件。虽然我认为该控件运行良好,但我认为仍有很大的改进空间。源代码也已提供,供那些想对其进行修改的人使用。我请求的是,如果您发现任何 bug 或对代码进行任何改进,也请在此处提交,以便大家都能享受您的工作!感谢您的阅读,也许下次文章再见。
致谢
我想感谢 NT Almond (Norm .net) 的文章 使用 GDI+ 和 C# 进行无闪烁绘图。UserControl
使用此技术在其面板上实现无闪烁绘图。