複数の画像を連結するだけであれば簡単にできます。しかし隣の画像の縦や横の長さとそろえたい、縦横の比を変えずに拡大縮小してから並べたい、そのまま並べるのではなく必要な部分だけトリミングしたものを並べたいということもあるのではないでしょうか?
簡単な動作でお気に入りの画像を自分でひとつの画像に編集したい、そんなかたのためにアプリを作成してみることにしました。
これまでスクロールバー付きのピクチャーボックスを自作してきましたが、これをつかって「ドラッグ&ドラッグでトリミングした画像を連結するアプリ」を作成します。
まずデータを管理するためのクラスを作成します。
Bitmapをファイルとして保存するときにBase64エンコードをします。そこでコンストラクタ内でそのための処理をおこなっています。WidthプロパティとHeightプロパティは描画するときの幅と高さです。
コンストラクタ内でSourceBitmapプロパティにBitmapがセットされるのですが、ファイルを読みだしたときはnullになっています。そこでSourceBitmap == nullのときはSourceBitmapBase64からBitmapを生成してそれを返します。
BitmapForShowプロパティはトリミングされて実際に描画されるBitmapです。X、YプロパティはどこにトリミングされたBitmapが表示されるかを示すもので、Width、Heightプロパティはそのときのサイズです。MarginLeft、MarginTop、MarginRight、MarginBottomプロパティは上下左右の切り捨てられた部分です。
拡大縮小するときに幅、高さのどちらかのみを指定して縦横比そのままで他方を求めるということもできます。EnableWidthのみがtrueのときは幅から高さを求めます。逆にEnableHeightプロパティのみがtrueのときは高さから幅を求めます。両方trueの場合は縦横比に縛られず両方をそれぞれ指定することができます。両方がfalseの場合はBitmapForShowの幅と高さをそのまま使います。
ではDataクラスを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
public class Data { public Data() { } public Data(Bitmap bitmap) { _sourceBitmap = bitmap; MemoryStream ms = new MemoryStream(); SourceBitmap.Save(ms, ImageFormat.Png); byte[] vs = ms.ToArray(); SourceBitmapBase64 = Convert.ToBase64String(vs); Width = bitmap.Width; Height = bitmap.Height; } public string SourceBitmapBase64 { get; set; } Bitmap _sourceBitmap = null; public Bitmap SourceBitmap { get { if (_sourceBitmap != null) return _sourceBitmap; else { byte[] vs = Convert.FromBase64String(SourceBitmapBase64); using (MemoryStream ms = new MemoryStream(vs)) { _sourceBitmap = new Bitmap(ms); return _sourceBitmap; } } } } Bitmap _bitmapForShow = null; [System.Xml.Serialization.XmlIgnoreAttribute] public Bitmap BitmapForShow { get { if (_bitmapForShow == null) _bitmapForShow = GetShowBitmap(GetTrimmedBitmap()); return _bitmapForShow; } set { _bitmapForShow = value; } } public string Name { get; set; } public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } public int MarginLeft { get; set; } public int MarginTop { get; set; } public int MarginRight { get; set; } public int MarginBottom { get; set; } public bool EnableWidth { get; set; } public bool EnableHeight { get; set; } public bool Hidden { get; set; } Bitmap GetShowBitmap(Bitmap srcBitmap) { if (srcBitmap == null) return null; int newWidth = Width; int newHeight = Height; double d = 1d * srcBitmap.Width / srcBitmap.Height; if (EnableWidth && !EnableHeight) { newHeight = (int)(Width / d); } if (!EnableWidth && EnableHeight) { newWidth = (int)(Height * d); } if (!EnableWidth && !EnableHeight) { return new Bitmap(srcBitmap); } Bitmap newBitmap = new Bitmap(newWidth, newHeight); Graphics g = Graphics.FromImage(newBitmap); g.DrawImage(srcBitmap, new Rectangle(0, 0, newWidth, newHeight)); g.Dispose(); return newBitmap; } Bitmap GetTrimmedBitmap() { Bitmap bitmap = SourceBitmap; if (SourceBitmap == null) return null; Rectangle rect = GetTrimmedRectangle(new Rectangle(0, 0, bitmap.Width, bitmap.Height)); if (rect.Width <= 0 || rect.Height <= 0) return null; Bitmap newBitmap = new Bitmap(rect.Width, rect.Height); Graphics g = Graphics.FromImage(newBitmap); g.DrawImage(bitmap, new Rectangle(0, 0, rect.Width, rect.Height), rect, GraphicsUnit.Pixel); g.Dispose(); return newBitmap; } Rectangle GetTrimmedRectangle(Rectangle rect) { int left = MarginLeft; int top = MarginTop; int right = MarginRight; int bottom = MarginBottom; return new Rectangle(left, top, rect.Width - left - right, rect.Height - top - bottom); } } |
次にデザイナで以下のようなものを作ります。
TreeViewにファイルをドラッグ&ドロップするとアイテムとして追加されるとともに右側のScrollPictureBoxにBitmapが表示されます。複数同時にドラッグ&ドロップすると同じ位置に重なってしまいますが、これらはドラッグ&ドロップすることで移動させることができます。
重なり合っているとどれがどれなのかわからなくなります。そこで現在選択されているTeeNodeに対応するBitmapがどれなのかがわかるように左側下部のPictureBoxにも表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); PictureBox1.SizeMode = PictureBoxSizeMode.Zoom; PictureBox2.SizeMode = PictureBoxSizeMode.Zoom; // アップダウンコントロールで表示倍率を変更できるようにする UpDownExpansionRate.Value = 100; UpDownExpansionRate.Minimum = 1; UpDownExpansionRate.Maximum = 1000; UpDownExpansionRate.ValueChanged += NumericUpDown1_ValueChanged; // ファイルをドラッグドロップで追加できるようにする TreeView1.DragOver += TreeView1_DragOverFiles; TreeView1.DragDrop += TreeView1_DragDropFiles; // 選択されているTreeNodeに対応するイメージを表示する TreeView1.AfterSelect += TreeView1_AfterSelect; // Bitmapをドラッグドロップで移動できるようにする ScrollPictureBox1.PictureBoxMouseDown1 += ScrollPictureBox1_PictureBoxMouseDown1; ScrollPictureBox1.PictureBoxMouseMove1 += ScrollPictureBox1_PictureBoxMouseMove1; ScrollPictureBox1.PictureBoxMouseUp1 += ScrollPictureBox1_PictureBoxMouseUp1; // アイテムをダブルクリックしたら編集用のフォームを表示する TreeView1.MouseDoubleClick += TreeView1_MouseDoubleClick; // アイテムをドラッグドロップで移動できるようにする TreeView1.AllowDrop = true; TreeView1.ItemDrag += TreeView1_ItemDrag; TreeView1.DragOver += TreeView1_DragOver; TreeView1.DragDrop += TreeView1_DragDrop; } } |
まずアップダウンコントロールで表示倍率を変更できるようにします。
1 2 3 4 5 6 7 8 9 10 |
public partial class Form1 : Form { // アップダウンコントロールで表示倍率を変更できるようにする private void NumericUpDown1_ValueChanged(object sender, EventArgs e) { double rate = (double)UpDownExpansionRate.Value / 100; ScrollPictureBox1.Expansionrate = rate; } } |
TreeViewの上にファイルをドラッグドロップするとTreeNodeが追加されるとともに、追加されたBitmapがSchoolPictureBoxに表示されるようにします。
TreeNodeが追加されるときにBitmapをDataクラスのコンストラクタに渡して、Dataオブジェクトを生成します。そしてこれをTreeNode.Tagにセットします。これで追加された画像とTreeNodeを関連付けることができます。
上記が終わったら自作メソッド ShowBitmaps()でSchoolPictureBoxに描画させます。
GetDatas()メソッド内でTreeNodeに関連付けられたDataオブジェクトのリストを返してしますが、Reverse()メソッドで順番を逆にしています。これはTreeNodeの上にあるものを上側(手前)に描画したいからです。先に描画したものはあとから描画されたものに隠されてしまいます。上にあるものを手前に描画するために順番を逆にしているのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
public partial class Form1 : Form { // ファイルをドラッグドロップで追加できるようにする private void TreeView1_DragOverFiles(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy; } private void TreeView1_DragDropFiles(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); foreach(string path in files) { if (IsImageFile(path)) { TreeNode node = new TreeNode(); node.Text = path; TreeView1.Nodes.Insert(0, node); SetDataToTreeNode(node, path); } } ShowBitmaps(); } } bool IsImageFile(string path) { try { Bitmap bitmap = new Bitmap(path); bitmap.Dispose(); return true; } catch { return false; } } // 追加された画像とTreeNode.Tagプロパティを関連付ける void SetDataToTreeNode(TreeNode newNode, string filePath) { Bitmap bitmap = GetImageFromFile(filePath); Data data = new Data(bitmap); newNode.Tag = data; } Bitmap GetImageFromFile(string path) { try { Bitmap bitmap = new Bitmap(path); Bitmap retBitmap = new Bitmap(bitmap); bitmap.Dispose(); return retBitmap; } catch { return null; } } void ShowBitmaps() { List<Data> datas = GetDatas(); ScrollPictureBox1.ClearMovableBitmap(); foreach (Data data in datas) { if (data.Hidden) continue; MovableBitmap movableBitmap = new MovableBitmap(data.BitmapForShow); movableBitmap.X = data.X; movableBitmap.Y = data.Y; ScrollPictureBox1.AddMovableBitmap(movableBitmap); } } List<Data> GetDatas() { List<Data> datas = new List<Data>(); foreach (TreeNode node in TreeView1.Nodes) { Data data = (Data)node.Tag; data.Name = node.Text; datas.Add(data); } datas.Reverse(); return datas; } } |
次に選択されているTreeNodeに対応するBitmapを左側下部のPictureBoxに描画します。左側がファイルから取得されたBitmap、右側がトリミングされたBitmapです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { // 選択されているTreeNodeに対応するイメージを表示する private void TreeView1_AfterSelect(object sender, TreeViewEventArgs e) { TreeNode node = TreeView1.SelectedNode; if (node != null) { PictureBox1.Image = ((Data)node.Tag).SourceBitmap; PictureBox2.Image = ((Data)node.Tag).BitmapForShow; } } } |
次にBitmapをドラッグ&ドロップで移動できるようにします。ドラッグ&ドロップで移動できるのは現在選択されているTreeNodeに対応するBitmapがクリックされたときです。
ScrollPictureBox内でクリックされるとPictureBoxMouseDown1イベントが発生します。自作メソッド IsInRectangle(Rectangle rect, Point pt)でクリックされた点が現在選択されているTreeNodeに対応するBitmapが表示されている矩形内部かどうかを判定し、矩形内部であればそのBitmapの位置と対応するDataオブジェクトを保存します。そしてPictureBoxMouseMove1イベントが発生したらイベントハンドラに渡される引数から移動量を知ることができるのでそのぶんだけ移動させます。
実際の移動の処理はMovingData.XとMovingData.Yを書き換えてShowBitmaps()メソッドを呼ぶだけです。このとき後述するダイアログ(トリミング処理をするためのもの。後述)が存在する場合はその値も変更させます。
マウスボタンが離されたら MovingData に nullを代入してそれ以降のマウス移動にともなう処理がおきないようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public partial class Form1 : Form { // Bitmapをドラッグドロップで移動できるようにする Data MovingData = null; int oldX = 0; int oldY = 0; private void ScrollPictureBox1_PictureBoxMouseDown1(object sender, PictureBoxMouseEventArgs e) { if (TreeView1.SelectedNode == null) return; Data data = (Data)TreeView1.SelectedNode.Tag; Rectangle rectangle = new Rectangle(data.X, data.Y, data.Width, data.Height); Point point = new Point(e.PixelX, e.PixelY); if (IsInRectangle(rectangle, point)) { MovingData = data; oldX = data.X; oldY = data.Y; } } private void ScrollPictureBox1_PictureBoxMouseMove1(object sender, PictureBoxMouseEventArgs e) { if (MovingData != null) { MovingData.X = oldX + e.PixelMoveX; MovingData.Y = oldY + e.PixelMoveY; ShowBitmaps(); // ダイアログ(後述)が存在する場合はその値も変更させる FormForTrimming f = FormForTrimmings.FirstOrDefault(x => x.SourceBitmap == MovingData.SourceBitmap); if (f != null) { f.BitmapX = MovingData.X; f.BitmapY = MovingData.Y; } } } private void ScrollPictureBox1_PictureBoxMouseUp1(object sender, PictureBoxMouseEventArgs e) { MovingData = null; } bool IsInRectangle(Rectangle rect, Point pt) { if (pt.X < rect.Left) return false; if (pt.Y < rect.Top) return false; if (rect.Right < pt.X) return false; if (rect.Bottom < pt.Y) return false; return true; } } |
TreeNodeをドラッグ&ドロップすると位置を入れ替えることができるようにします。TreeNodeの位置を入れ替えたらShowBitmaps()メソッドを実行して再描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public partial class Form1 : Form { // ドラッグドロップに対応させる private void TreeView1_ItemDrag(object sender, ItemDragEventArgs e) { TreeView1.DoDragDrop(e.Item, DragDropEffects.Move); } private void TreeView1_DragOver(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(typeof(TreeNode))) e.Effect = DragDropEffects.Move; } private void TreeView1_DragDrop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(typeof(TreeNode))) { TreeNode node = (TreeNode)e.Data.GetData(typeof(TreeNode)); // マウスボタンが離された場所にTreeNodeは存在するか? // 存在するならそのひとつ上に移動させる Point pt = TreeView1.PointToClient(new Point(e.X, e.Y)); TreeNode targetNode = TreeView1.GetNodeAt(pt); int targetIndex = TreeView1.Nodes.IndexOf(targetNode); if (targetIndex != -1) { // いったん移動対象になるTreeNodeを取り除き、 // もう一度TreeView1.Nodes.IndexOf(targetNode)を実行する // ここが新しい挿入位置 TreeView1.Nodes.Remove(node); targetIndex = TreeView1.Nodes.IndexOf(targetNode); TreeView1.Nodes.Insert(targetIndex, node); TreeView1.SelectedNode = node; ShowBitmaps(); } } } } |
メニューで[削除]を選択するとTreeNodeを削除できるようにします。TreeNodeを削除したらShowBitmaps()メソッドを実行して再描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form1 : Form { private void MenuItemdDeleteNode_Click(object sender, EventArgs e) { TreeNode node = TreeView1.SelectedNode; if (node != null) { TreeView1.Nodes.Remove(node); ShowBitmaps(); // FormForTrimmingsに関しては後述 FormForTrimming f = FormForTrimmings.FirstOrDefault(x => x.SourceBitmap == ((Data)node.Tag).SourceBitmap); if (f != null) { FormForTrimmings.Remove(f); f.Dispose(); } } } } |
ファイルとして保存したり読み出すことができるようにします。
1 2 3 4 |
public class Doc { public List<Data> Datas = new List<Data>(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
using System.Xml.Serialization; using System.IO; public partial class Form1 : Form { // 編集中のファイルのパス string FilePath = ""; private void MenuItemOpenFile_Click(object sender, EventArgs e) { OpenFileDialog dialog = new OpenFileDialog(); dialog.Filter = "データファイル(*.xml)|*.xml"; // 編集中のファイルのパスが存在するなら選択状態にしておく if (File.Exists(FilePath)) dialog.FileName = FilePath; if (dialog.ShowDialog() == DialogResult.OK) { XmlSerializer xml = new XmlSerializer(typeof(Doc)); StreamReader sr = new StreamReader(dialog.FileName); Doc doc = (Doc)xml.Deserialize(sr); sr.Close(); TreeView1.Nodes.Clear(); // FormForTrimmingsに関しては後述 foreach (FormForTrimming f in FormForTrimmings) f.Dispose(); FormForTrimmings.Clear(); foreach (Data data in doc.Datas) { TreeNode node = new TreeNode(); node.Tag = data; node.Text = data.Name; TreeView1.Nodes.Add(node); } ShowBitmaps(); FilePath = dialog.FileName; } dialog.Dispose(); } private void MenuItemSaveFile_Click(object sender, EventArgs e) { SaveFileDialog dialog = new SaveFileDialog(); dialog.Filter = "データファイル(*.xml)|*.xml"; // 編集中のファイルのパスが存在するなら選択状態にしておく if(File.Exists(FilePath)) dialog.FileName = FilePath; if (dialog.ShowDialog() == DialogResult.OK) { Doc doc = new Doc(); List<Data> datas = GetDatas(); datas.Reverse(); doc.Datas = datas; XmlSerializer xml = new XmlSerializer(typeof(Doc)); StreamWriter sw = new StreamWriter(dialog.FileName); xml.Serialize(sw, doc); sw.Close(); FilePath = dialog.FileName; } dialog.Dispose(); } } |
最後に合成された画像をPngファイルとして保存するメソッドを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public partial class Form1 : Form { private void MenuItemSaveImageFile_Click(object sender, EventArgs e) { SaveFileDialog dialog = new SaveFileDialog(); dialog.Filter = "PNG(*.png)|*.png"; if (dialog.ShowDialog() == DialogResult.OK) { List<Data> datas = GetDatas().Where(x => !x.Hidden).ToList(); if (datas.Count > 0) { int width = datas.Max(x => x.X + x.Width); int height = datas.Max(x => x.Y + x.Height); Bitmap bitmap = new Bitmap(width, height); Graphics g = Graphics.FromImage(bitmap); foreach (Data data in datas) { g.DrawImage(data.BitmapForShow, new Point(data.X, data.Y)); } g.Dispose(); bitmap.Save(dialog.FileName, System.Drawing.Imaging.ImageFormat.Png); bitmap.Dispose(); } } dialog.Dispose(); } } |