1枚の画像について複数のリンクが設定されたものをクリッカブルマップといいます。そこで画像を読み込ませてクリックして領域を選択してリンク先を設定したらコピペするだけでクリッカブルマップをつくることができるアプリケーションをつくります。
Contents
クリッカブルマップの構造
生成されるコードは以下のようなものになります。shapeを=”rect”や”circle”にすることもできますが、今回は”polygon”(多角形)だけにします。
1 2 3 4 5 |
<map name="map01"> <area href="1.html" shape="polygon" coords="x1,y1 x2,y2 x3,y3 ..."> <area href="2.html" shape="polygon" coords="x11,y12 x12,y12 x13,y13 ..."> </map> <img src="/image.png" border="0" usemap="#map01"> |
それから実験で使うイメージはここからダウンロードしました。
四国地方 地図 イラストイラスト – No: 2554054/無料イラストなら「イラストAC」
これがダウンロードした画像です。
四国の各県をクリックすると県庁のホームページに飛ぶようなものをつくります。
まずユーザーコントロールをつくり、ここにPictureBoxをはりつけます。
ユーザーコントロールにAutoScrollを設定して、PictureBoxのSizeModeプロパティをPictureBoxSizeMode.AutoSizeにすれば大きな画像であっても自動的にスクロールバーがつきます。あとはクリックされた場所を記憶できるようにしておけばクリッカブルマップになるHTMLコードを生成できそうです。
それからフォームはこんな感じになります。左側に貼り付けているのが上で作成したユーザーコントロール(クラス名はUserControlImage)です。
UserControlImageクラス
まずクリックされた座標をリストのなかに格納して多角形を描画するためのクラスをつくります。
Polygonクラス
コンストラクタには多角形を描画するための座標とリンク先のurlを渡します。あとはGetPointsメソッドで座標のリストのコピーを返し、GetTagメソッドで多角形の座標をHTMLに変換した文字列(<area href=”url.html” shape=”polygon” coords=”x1,y1 x2,y2 x3,y3 …”>)を返します。
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 |
public class Polygon { List<Point> Points = new List<Point>(); string Url = ""; public Polygon(List<Point> points, string url) { Points = points.ToList(); Url = url; } public List<Point> GetPoints() { return Points.ToList(); } public string GetTag() { List<int> vs = new List<int>(); foreach (Point pt in Points) { vs.Add(pt.X); vs.Add(pt.Y); } string coords = String.Join(",", vs.ToArray()); string str = $"<area href=\"{Url}\" shape=\"polygon\" coords=\"{coords}\">"; return str; } } |
UserControlImageクラスの機能
次にユーザーコントロールですが、以下の処理をおこないます。
ファイルをドラッグアンドドロップしたら画像ファイルであれば読み込んでPictureBoxに表示させる
画像が読み込まれた状態でクリックされたら座標を格納する
親フォームで[確定]ボタンがクリックされたらクリックされた座標で多角形が構成されているならPolygonをリストに格納する
親フォームで[HTMLタグを生成]がクリックされたらクリッカブルマップに対応したHTMLタグを出力する
やりたい処理は以上となります。
UserControlImageクラスの初期化
最初にコンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class UserControlImage : UserControl { public UserControlImage() { InitializeComponent(); // PictureBoxにスクロールバーをつける pictureBox1.Location = new Point(0, 0); pictureBox1.SizeMode = PictureBoxSizeMode.AutoSize; // イベントハンドラを追加する pictureBox1.MouseDown += PictureBox1_MouseDown; pictureBox1.Paint += PictureBox1_Paint; // ドラッグアンドドロップに対応させる this.AllowDrop = true; // 確定した多角形を囲むPenを生成する CreatePens(); } } |
多角形を囲むPenを生成する
確定した多角形を囲むPenを生成する処理を示します。これは最初に1回だけやればよいのでコンストラクタの中で実行することにして、うっかり2回実行するようなことになってもMyPensのなかに要素がある場合はなにもしません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public partial class UserControlImage : UserControl { List<Pen> MyPens = new List<Pen>(); public void CreatePens() { if (MyPens.Count != 0) return; MyPens.Add(new Pen(Color.FromArgb(0xff, 0x00, 0x00))); MyPens.Add(new Pen(Color.FromArgb(0x00, 0xff, 0x00))); MyPens.Add(new Pen(Color.FromArgb(0x00, 0x00, 0xff))); MyPens.Add(new Pen(Color.FromArgb(0xff, 0xff, 0x00))); MyPens.Add(new Pen(Color.FromArgb(0x00, 0xff, 0xff))); MyPens.Add(new Pen(Color.FromArgb(0xff, 0x00, 0xff))); MyPens.Add(new Pen(Color.FromArgb(0x80, 0x00, 0x00))); MyPens.Add(new Pen(Color.FromArgb(0x00, 0x80, 0x00))); MyPens.Add(new Pen(Color.FromArgb(0x00, 0x00, 0x80))); MyPens.Add(new Pen(Color.FromArgb(0x80, 0x80, 0x00))); MyPens.Add(new Pen(Color.FromArgb(0x00, 0x80, 0x80))); MyPens.Add(new Pen(Color.FromArgb(0x80, 0x00, 0x80))); } } |
ファイルがドラッグされているときの処理
次にドラッグされているときの処理を示します。ドラッグされているものが本当にファイルなのか、ファイルである場合、画像ファイルなのかを調べて適切なDragEventArgs.Effectを設定しなければなりません。
ファイルであるかどうかはDragEventArgs.Data.GetDataPresent(DataFormats.FileDrop)がtrueかどうかで調べられます。ファイルである場合はImage.FromFileメソッドにファイルパスを渡し例外が発生しなければ画像ファイルであると判断します。
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 |
public partial class UserControlImage : UserControl { protected override void OnDragOver(DragEventArgs drgevent) { if (IsDragOverImageFile(drgevent)) drgevent.Effect = DragDropEffects.Copy; else drgevent.Effect = DragDropEffects.None; base.OnDragOver(drgevent); } bool IsDragOverImageFile(DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); string file = files[0]; try { Image image = Image.FromFile(file); image.Dispose(); // 画像ファイルであることが確認されたら // ファイルがロックされたままにならないようにimage.Dispose()を実行する return true; } catch { // 例外が発生したら画像ファイルではないファイルなのでfalseを返す return false; } } return false; } } |
ファイルのドロップされたときの処理
ファイルがドロップされたらそこがPictureBoxであってもユーザーコントロールであっても、ユーザーコントロールでOnDragDropメソッドが呼び出されます。この場合は画像ファイルのパスをフィールド変数に格納するとともに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 38 39 40 |
public partial class UserControlImage : UserControl { string FilePath = ""; List<Point> Points = new List<Point>(); // 多角形として確定していない座標を格納する List<Polygon> Polygons = new List<Polygon>(); // 確定した多角形を格納する protected override void OnDragDrop(DragEventArgs drgevent) { SetImagePictureBox(drgevent); base.OnDragDrop(drgevent); } void SetImagePictureBox(DragEventArgs drgevent) { if (drgevent.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])drgevent.Data.GetData(DataFormats.FileDrop); string file = files[0]; try { Points.Clear(); Polygons.Clear(); Image image = Image.FromFile(file); Bitmap bitmap = new Bitmap(image); image.Dispose(); pictureBox1.Image = bitmap; var info = new System.IO.FileInfo(file); FilePath = info.Name; // HTMLタグを生成するときにファイル名(フルパスではなく)が必要なので保存しておく } catch { // 画像ファイルであることを確認してからのドロップなので例外はないはず・・・ } } } } |
PictureBoxがクリックされたときと再描画の処理
PictureBoxがクリックされたときの処理を示します。
クリックされたらその座標を調べてフィールド変数Pointsのなかに格納します。そしてPictureBox.Invalidateメソッドを実行します。
PictureBox.Invalidateメソッドを実行されたらPointsに座標が格納されている場合、それが1つだけだったら点を描画し(実際には幅高さ1の矩形)、複数の場合は順番に直線で結びます。またすでに確定している多角形が存在するのであれば、別の色で多角形を描画します。
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 UserControlImage : UserControl { private void PictureBox1_MouseDown(object sender, MouseEventArgs e) { Points.Add(e.Location); pictureBox1.Invalidate(); } private void PictureBox1_Paint(object sender, PaintEventArgs e) { Bitmap bitmap = (Bitmap)pictureBox1.Image; if (bitmap == null) return; if (Points.Count == 1) e.Graphics.DrawRectangle(Pens.Black, Points[0].X, Points[0].Y, 1, 1); else if (Points.Count > 1) e.Graphics.DrawLines(Pens.Black, Points.ToArray()); int i = 0; foreach (Polygon polygon in Polygons) { int index = i % MyPens.Count; e.Graphics.DrawLines(MyPens[index], polygon.GetPoints().ToArray()); i++; } } } |
多角形を確定させる処理
親フォームで[確定]が選択されたら格納されている座標が3以上である場合は多角形が形成されています。この場合は最初にPointsに格納された座標と最後に格納された座標をつないで多角形をつくり、これを確定します。そしてそれをPolygonsに格納し、Pointsは次の多角形を設定できるようにクリアします。処理が正常におこなわれた場合はtrueを返し、多角形が確定できない場合はfalseを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class UserControlImage : UserControl { public bool Confirm(string url) { // 多角形が形成されている場合はPolygonsに格納してtrueを返す // そうでないならfalseを返す if (Points.Count > 2) { Points.Add(Points[0]); Polygons.Add(new Polygon(Points, url)); Points.Clear(); pictureBox1.Invalidate(); return true; } else return false; } } |
結果を返すための処理
親フォームで[HTMLタグを生成]がクリックされたらクリッカブルマップに対応したHTMLタグを出力します。ユーザーコントロール側でやることはクリッカブルマップに対応したHTMLタグの文字列を返すだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class UserControlImage : UserControl { public string GetTag() { StringBuilder sb = new StringBuilder(); DateTime dt = DateTime.Now; string mapName = $"{dt.Ticks}"; sb.Append($"<map name=\"map{dt.Ticks}\">\n") ; foreach (Polygon polygon in Polygons) { sb.Append(polygon.GetTag() + "\n"); } sb.Append($"</map>\n"); sb.Append($"<img src=\"./{FilePath}\" border=\"0\" usemap=\"#map{dt.Ticks}\">"); return sb.ToString(); } } |
データをクリアする処理
Clearメソッドは現在確定している多角形はそのままで、現在Pointsに格納されている座標だけをクリアします。AllClearメソッドは現在確定している多角形もクリアするとともにPictureBoxに格納されているImageもクリアします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class UserControlImage : UserControl { public void Clear() { Points.Clear(); pictureBox1.Invalidate(); } public void AllClear() { Points.Clear(); Polygons.Clear(); pictureBox1.Image = null; } } |
仕上げ 親フォームにおける処理
あとは親フォームでボタンがクリックされたときの処理を定義するだけです。
[確定]ボタンがクリックされたら多角形を確定する処理をおこないます。リンク先が設定されていなかったり、座標が3点以上指定されていないため多角形を確定させることができない場合はエラーメッセージを表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { private void ButtonConfirm_Click(object sender, EventArgs e) { string url = textBox1.Text; if (url == "") { MessageBox.Show("リンク先が設定されていません", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (!this.UserControlImage1.Confirm(url)) { MessageBox.Show("3点以上設定してください", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } textBox1.Text = ""; } } |
[クリア]、[全クリア]がクリックされたらデータをクリアする処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { private void ButtonClear_Click(object sender, EventArgs e) { textBox1.Text = ""; this.UserControlImage1.Clear(); } private void ButtonAllClear_Click(object sender, EventArgs e) { textBox1.Text = ""; this.UserControlImage1.AllClear(); } } |
クリッカブルマップをレスポンシブ対応させるには
[HTMLタグを生成]ボタンがクリックされたらHTMLタグを取得します。ただレスポンシブ対応にしたい場合はそのようなコードを取得しなければなりません。そこでチェックボックスにチェックが付いている場合はレスポンシブ対応のコードも追加します。
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 |
public partial class Form1 : Form { private void ButtonGetHtmlTag_Click(object sender, EventArgs e) { if(checkBox1.Checked) richTextBox1.Text = GetResponsiveScript() + UserControlImage1.GetTag(); else richTextBox1.Text = UserControlImage1.GetTag(); } // 長いけど画像のサイズが変更されても対応できるHTMLコードが取得できる string GetResponsiveScript() { return "<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n" + "<script>" + "// rwdImageMaps jQuery plugin v1.6\n" + "// Copyright (c) 2016 Matt Stow\n" + "// https://github.com/stowball/jQuery-rwdImageMaps\n" + "// http://mattstow.com\n" + "// Licensed under the MIT license\n" + ";(function($) {" + "$.fn.rwdImageMaps = function() {" + "var $img = this;" + "var rwdImageMap = function() {" + "$img.each(function() {" + "if (typeof($(this).attr('usemap')) == 'undefined') return;" + "var that = this, $that = $(that);" + "$('<img />').on('load', function() {" + "var attrW = 'width',attrH = 'height',w = $that.attr(attrW),h = $that.attr(attrH);" + "if (!w || !h) {" + "var temp = new Image();" + "temp.src = $that.attr('src');" + "if (!w) w = temp.width;" + "if (!h) h = temp.height;" + "}" + "var wPercent = $that.width()/100, hPercent = $that.height()/100," + "map = $that.attr('usemap').replace('#', '')," + "c = 'coords';" + "$('map[name=\"' + map + '\"]').find('area').each(function() {" + "var $this = $(this);" + "if (!$this.data(c)) $this.data(c, $this.attr(c));" + "var coords = $this.data(c).split(',')," + "coordsPercent = new Array(coords.length);" + "for (var i = 0; i < coordsPercent.length; ++i) {" + "if (i % 2 === 0) coordsPercent[i] = parseInt(((coords[i]/w)*100)*wPercent);" + "else coordsPercent[i] = parseInt(((coords[i]/h)*100)*hPercent);" + "}" + "$this.attr(c, coordsPercent.toString());" + "});" + "}).attr('src', $that.attr('src'));" + "});" + "};" + "$(window).resize(rwdImageMap).trigger('resize');" + "return this;" + "}; })(jQuery);" + "$(function() {$('img[usemap]').rwdImageMaps();})" + "</script>\n"; } } |
取得されたHTMLタグはRichTextBoxに表示されますが、ボタンをクリックしたらクリップボードにコピーされるようにしてしまえば便利かもしれません。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { private void button3_Click(object sender, EventArgs e) { string str = richTextBox1.Text; if(str != "") Clipboard.SetText(str); } } |