Coding4Fun:Rob 的 Image Shrinker
本页内容
Smartphone、Pocket PC 和 Sony Playstation Portable 等是非常有用的图片携带设备。但是,如今的照相机拍摄的图片要比移动设备的要求大得多。高象素使图片在大显示屏上的打印或预览效果非常好,但是显示屏较小时就不易达到这种效果。这些高分辨率图片文件在加载时需要很长时间并占用大量文件空间。(本文包含一些指向英文网站的链接) 许多图形程序(例如 Microsoft Digital Image Pro 10)具有批量编辑功能,从而使您可以批量调整图片的大小。如果您有 Portable Media Center,则可以使用 Media Player 10 在图像传输时调整其大小。但是,您真正需要的是这样一个程序:通过该程序可以调整图片大小并立即传输这些图片。因此,我编写了一个程序,我们可以研究一下它如何工作。 无论您只是想使用该程序还是想研究代码,入门都很轻松。如果您只是想使用该程序,我提供了一个用户手册。 使用应用程序我使用 Visual C# 2005 Express Edition 创建了该项目。打开相应的项目文件即可使用。如果您没有 Visual Studio,则可以从 http://msdn.microsoft.com/vstudio/express/visualcsharp/ 上免费获得一个副本。 初始下载的副本非常小,但安装时代码将达到 300 MB。 您的机器上安装 Visual Studio 后,便可以打开相应的目录并选择解决方案文件。在接下来的小节中,我们将详细介绍该程序的各个部分。给出的示例与源代码并不完全相同,但是我们希望能够将注意力集中在各个开发部分的重要方面。 设置图像的目标我使用 FolderBrowserDialog 来设置目标。这使用户可以找到传输图像文件的目标目录。我设置此选项还可以使用户能创建放置图像的新目录: outputDirDialog = new System.Windows.Forms.FolderBrowserDialog(); outputDirDialog.Description = "选择目标"; outputDirDialog.ShowNewFolderButton = true; outputDirDialog.ShowDialog(); 我使用 ShowDialog 方法,从而使用户在使用以下对话框选择目标时,程序的其余部分可以暂停: 图 1:浏览目标 用户打开“我的电脑”树时,可以看到所有驱动器盘符,包括存储设备的所有驱动器盘符。注意:该程序还必须处理用户未选择文件夹而是单击“取消”按钮的情况。在这种情况下,生成的路径长度将是 0,因此我们将显示相应的消息: if (outputDirDialog.SelectedPath.Length == 0) { statusLabel.Text = "未选择目标"; return; } 我们在表格的底部使用一个标签来将消息发送给用户。 获得源图像获知放置文件的位置之后,现在,我们需要选择一些文件以进行传输。OpenFileDialog 恰好可以进行此操作,因为可以将其配置为允许用户选择多个文件。该对话框还可以显示每个图片的缩略图,因此用户可以轻松地查看要传输的文件。 sourceFilesDialog = new OpenFileDialog(); sourceFilesDialog.Multiselect = true; sourceFilesDialog.Title = "选择要收缩的文件"; 这段代码将创建该对话框,并将其配置为允许选择多个文件。现在,我们可以将其配置为只显示要传输的图像文件: sourceFilesDialog.Filter = "图像文件 (*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|所有文件 (*.*)|*.*"; 筛选器字符串看上去相当复杂,但实际上非常简单。字符串中的各个元素以竖线字符隔开。每个筛选器均表示为一对项目、一个描述字符串后跟一列筛选器表达式。如果按以下方式表达看起来会更清楚: "图像文件 (*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|" + "所有文件 (*.*)|*.*" 上面的筛选器使用户可以选择 Bitmap、JPEG 或 GIF 图像文件。第二行使用户可以选择所有文件。对于每一行,竖线都会将要显示在用户面前的文字与应用到该选择的一列文件扩展名分开。 如果用户选择上面的筛选器,则只显示与 Bitmap、JPEG 或 GIF 图像匹配的文件名。如果选择下面的筛选器,则显示所有文件。 图 2:使用的筛选器 注意:仅仅因为文件带有特殊的扩展名,并不能说明该文件包含特殊的文件类型;我们的程序必须确保无效的文件内容不会引起问题。我们可以以后再介绍这一部分。 用户可以从目录中选择任意多个文件,甚至可以使用 CTRL+A 组合键选择全部文件。当用户单击“打开”按钮时,对话框将结束,然后可以处理文件并将其传输到目标设备中。 执行处理OpenFileDialog 将一列文件名作为字符串数组返回。现在,程序必须打开每个文件,加载文件中的位图,将位图大小调整为所需的尺寸,然后将该位图保存到目标目录中。方法 processFiles 可实现以上所有操作。 processFiles(sourceFilesDialog.FileNames, outputDirDialog.SelectedPath); 该方法会传递文件名数组和用于输出的目标路径。然后,它必须用同样的方式处理所有文件,并依次缩放和保存每个文件。 private void processFiles(string[] FileNames, string outputPath) { Bitmap dest = new Bitmap(size.width, size.height); foreach (string filename in FileNames) { Bitmap image; try { image = new Bitmap(filename); } catch { MessageBox.Show("加载位图时出错:" + filename); continue; } scaleBitmap(dest,image); string destFilename = outputPath + @"\" + System.IO.Path.GetFileNameWithoutExtension(filename) + ".jpg"; try { dest.Save(destFilename, System.Drawing.Imaging.ImageFormat.Jpeg); } catch { MessageBox.Show("保存位图时出错:" + destFilename); return; } } } 该方法将处理每个文件,并创建每个文件的位图,调用 scaleBitmap 方法来缩放位图,然后将结果保存回磁盘上。 注意:我已将一个异常处理程序应用于创建文件的位图。如果加载失败,将显示一个消息,方法开始处理下一个图像。还将一个 try catch 结构应用于保存操作。但是,如果该保存失败,这通常意味着下一个保存也可能失败,因为输出设备可能已满。因此,如果保存调用失败,则方法将返回而不是继续。 缩放位图缩放位图非常简单。可以使用一些绘图方法,来从一个图像向另一个图像绘制一个矩形。通过控制源和目标的大小,我们可以调整图像大小使其适合我们的设备。唯一的困难是我们必须处理源和目标图像的长宽比,以便我们不会剪切掉图像的任意部分。这与在宽屏幕电视上观看老电视节目(或在窄屏幕电视上观看新电视节目)时遇到的问题完全相同。调整大小方法必须使图片与屏幕大小吻合,并根据需要在其周围插入空白区域。 private Rectangle srcRect = new Rectangle(); private Rectangle destRect = new Rectangle(); private void scaleBitmap ( Bitmap dest, Bitmap src ) { destRect.Width = dest.Width; destRect.Height = dest.Height; using (Graphics g = Graphics.FromImage(dest)) { Brush b = new SolidBrush(backgroundColor); g.FillRectangle(b, destRect); srcRect.Width = src.Width; srcRect.Height = src.Height; float sourceAspect = (float)src.Width / (float)src.Height; float destAspect = (float)dest.Width / (float)dest.Height; if (sourceAspect > destAspect) { // 宽度大于高度时,保持宽度不变,按照比例增加高度(请注意:示例程序文件中的程序员注释使用的是英文,本文中将其译为中文是为了便于参考) destRect.Width = dest.Width; destRect.Height = (int)((float)dest.Width / sourceAspect); destRect.X = 0; destRect.Y = (dest.Height - destRect.Height) / 2; } else { // 高度大于宽度时,保持高度不变,按照比例增加宽度 destRect.Height = dest.Height; destRect.Width = (int)((float)dest.Height * sourceAspect); destRect.X = (dest.Width - destRect.Width) / 2; destRect.Y = 0; } g.DrawImage(src, destRect, srcRect, System.Drawing.GraphicsUnit.Pixel); } } 调整图像大小实际上非常简单。可以将源和目标矩形提供给 DrawImage 方法。以上方法确定了源矩形需要按照哪种方式进行缩放,然后将目标矩形调整到合适的大小和位置。方法最后对 DrawImage 的调用是执行实际工作的部分。注意:我们用来调整源和目标尺寸的矩形在方法外部声明。这样我们便不会为缩放的每个图像停止创建和删除新矩形。由于我们真正需要做的是在每次使用这些矩形时更改其尺寸,因此不必总是创建新矩形。 管理尺寸用户需要根据目标设备从一系列可能的宽度和高度中进行选择。执行此操作的最佳屏幕组件是 ComboBox,该组件带有一个项目列表,用户可以从中选取一个项目。 图 3:选择输出格式 管理输出格式的最佳方式是创建一个特殊的类,来保存尺寸信息。然后,我们可以将此类型的实例数组提供给 ComboBox,用户可以从中选取一个实例。 public class OutputSize { public int width; public int height; string name; public override string ToString() { return name + " " + width.ToString() + " x " + height.ToString(); } public OutputSize(string inName, int inWidth, int inHeight) { name = inName; width = inWidth; height = inHeight; } } OutputSize 类型包含用于识别该类型的 name 属性以及高度和宽度值。它提供的 ToString 方法用于提供显示在 ComboBox 中的文字。将 width 和 height 属性设置为 public,从而使其他类中的方法可以使用这些属性。最后,它使用了一个构造函数,使我们可以设置所有值。现在,我们可以创建该类的一个实例数组,可以用来配置 ComboBox: private OutputSize[] resolutionSettings = new OutputSize[] { new OutputSize ( "Pocket PC", 640, 480 ), new OutputSize ( "QVGA", 320, 240 ), new OutputSize ( "PSP", 480, 272 ), new OutputSize ( "Smartphone", 176, 180) }; 如果您要将其他尺寸添加到该程序中,则这些尺寸正好可以放置在该数组中,并被自动选取和使用。将设置添加到 ComboBox 中非常方便: resolutionComboBox.DataSource = resolutionSettings; 这是 Windows 窗体的一项非常强大的功能。ComboBox 恰好可以引入这些设置,并使用数组中的值填充它的选择。当我们要获得现有的选择时,只需获取该选择并将其转换为我们已识别的实际类型: OutputSize size = resolutionComboBox.SelectedItem as OutputSize; 现在,我们可以使用 size 实例的 width 和 height 属性来控制缩放。 设置背景色当图像的长宽比不是很合适时,用户可以选择背景色填充在图像的四周。该颜色将存储为窗体的成员: private Color backgroundColor = Color.White; 背景色最初设置为白色,但是用户可以根据喜好选择其他颜色。我使用 ColorDialog 对话框来使用户进行此操作: backColorDialog = new ColorDialog(); backColorDialog.SolidColorOnly = true; backColorDialog.Color = backgroundColor; backColorDialog.ShowDialog(); 该对话框使用户可以选择颜色: 图 4:选择颜色 用户选择颜色后,可以将其用作图像的背景(在 scaleBitmap 方法中作为背景来绘制)。 预览图像用户可能希望在传输图像时查看预览的图像。使用应用程序窗体上的 PictureBox 组件便可以轻松查看。我们将 PictureBox 的背景色设置为所选的背景,以使预览图像看起来尽可能地接近传输图像。PictureBox 上的图像从缩放的图像中进行设置。 previewPictureBox.Image = dest; 我们必须对 PictureBox 执行的唯一其他操作就是确保将它的 SizeMode 属性设置为 Zoom,以便预览图像可以准确地填满预览窗口。 显示进度另一个有用功能就是进度条。传输大量文件时,用户将渴望获得一些有关程序传输进度的提示。用目前已传输的文件数除以所选的文件总数,我们可以计算出进度。这会产生一个分数,我们可以乘以 100 以生成一个进度条尺寸的百分比值。 loadProgressBar.Value = (int)(100 * ((float)fileCount / (float)noOfFiles)); 解决死锁难题该程序可以正常工作,但确实有一个问题。有时需要相当长的时间才能完成图像传输。在该过程中,Visual Studio 提供的错误跟踪可能会判定该程序已经停止。然后,它将引发导致程序失败的异常。关闭该异常并不困难;我们需要找到“异常”项(位于“调试”菜单项上),然后清除 ContextSwitchDeadlock 异常旁边的“抛出”框,从而显示以下对话框: 图 5:关闭 ContextSwitchDeadlock 异常 完成这些操作后,即使传输量相当大,程序都将正常运行。 完成的程序完成的程序可以正常工作。我已经成功地将 127 张图片单向传输到我的 Playstation Portable 上,而且我发现了除了图片之外的惊喜 — 127 张图片只占用了 3.7 MB 的空间。但是,您还可以使用大量增强功能。 自动 Playstation Portable 检测Playstation Portable 在它的存储设备上有一个非常特殊的文件排列。实际上,您必须将图片放到 \PSP\PHOTO 目录下,否则它们将无法显示(以图 1 中的路径为例)。可以使程序检查系统的每个驱动器,自动查找 PSP 设备,并相应地进行自身配置。 拖放图像传输除了从文件对话框中选取图像外,还可以使程序接受拖放到窗体中的文件。然后,可以缩放和传输这些图像。 自动横向/纵向旋转Smartphone 具有纵向格式显示屏:它的高度大于宽度,这与大多数图片(这些图片为横向)相反。这意味着编写的程序不会充分利用屏幕。如果图片在传输时可以旋转,这将非常有用,从而使这些图片可以尽可能地填满显示屏。或者,可以向用户提供选项,允许他们在传输前旋转 Smartphone 图片,以便它们可以显示出最佳效果。 Rob Miles 是英国赫尔大学计算机系的讲师,该系的网址为 /http://www.net.dcs.hull.ac.uk/。他从一开始就从事编程工作,并且一直很喜欢这项工作。他目前在该系讲授 C# 课程及大量的软件工程课程。空闲时间里,他会在自己的博客 (http://www.crazyworldofrobmiles.com) 里写些随笔,或在他的 XBOX 360 上狂玩游戏。他还喜欢 Smartphone 和 Cheese。 |