2年以上前のことですが、ランチャーアプリをつくりました。
その記事にコメントが来ました。
ドラッグアンドドロップでアイコンの入れ替えができるようになると最高なのですが、可能なのでしょうか?
もちろん可能です。このアプリはC# Microsoft .NET Frameworkで作られています。ただ.NET Frameworkバージョン4.8をもってメジャーアップデートを終了することがアナウンスされているので、今回は.NET Windowsフォームアプリで作成します。.NET Frameworkのときは問題なく動いていたコードではダメな部分もあったので作り直すことにします。
基本的な作り方は同じです。PanelのうえにPictureBoxを貼り付けたものをフォーム上にならべます。ファイル(ショートカットでもOK)をドラッグアンドドロップするとPictureBoxのうえにアイコンが表示され、これをクリックするとファイルが開くというものです。
Contents
PanelExクラスの定義
Panelクラスを継承してPanelExクラスをつくります。
まずコンストラクタと主なフィールド変数、定数を示します。
大きさは縦横32ピクセルです。ドラッグアンドドロップで並べ替えをするときにどれが移動対象になるのかがわかるようにIndexというフィールド変数を定義しています。またクリックするとファイルやフォルダを開かないといけないのでファイルのパスを格納する_pathというフィールド変数も定義しています。
コンストラクタが呼び出されるとPictureBoxが生成され、Panelのうえを完全に覆います。そのためクリックやドラッグやドロップのイベントはPictureBoxで発生します。これに対応できるようにイベントハンドラを追加しています。イベントハンドラの名前の最後に2がついているものはドラッグアンドドロップで順番を変更するときに必要になるものです。
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 |
public class PanelEx : Panel { Form1 _form; PictureBox _pictureBox = new PictureBox(); string _path = ""; public const int PanelWidth = 32; public const int PanelHeight = 32; public int Index = 0; public PanelEx(Form1 form, int index) { _form = form; this.BorderStyle = BorderStyle.FixedSingle; this.Size = new Size(PanelWidth, PanelHeight); _pictureBox.AllowDrop = true; _pictureBox.Parent = this; _pictureBox.Dock = DockStyle.Fill; _pictureBox.Click += PictureBox_Click; _pictureBox.MouseMove += PictureBox_MouseMove; _pictureBox.DragOver += PictureBox_DragOver; _pictureBox.DragDrop += PictureBox_DragDrop; _pictureBox.MouseDown += PictureBox_MouseDown2; _pictureBox.MouseUp += PictureBox_MouseUp2; _pictureBox.MouseMove += PictureBox_MouseMove2; _pictureBox.DragOver += PictureBox_DragOver2; ; _pictureBox.DragDrop += PictureBox_DragDrop2; this.Index = index; } } |
ファイルやフォルダがドラッグアンドドロップされたとき
ファイルやフォルダがドラッグアンドドロップされたときの処理を示します。ドラッグされているものやドロップされたものがDataFormats.FileDropでない場合は無視です。
ファイルまたはフォルダ(ショートカットも含む)が1つだけドラッグされている場合はe.Effect = DragDropEffects.Copy;とすることでマウスポインタの形状をコピーをするときの形に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class PanelEx : Panel { private void PictureBox_DragOver(object? sender, DragEventArgs e) { if (e.Data != null && e.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); if(files.Length == 1) e.Effect = DragDropEffects.Copy; } } } |
ドロップ時の処理を示します。ドロップされたものがファイルまたはフォルダ(ショートカットも含む)であり、その数が1つだけだったときはSetPathメソッドを呼び出して登録の処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PanelEx : Panel { private void PictureBox_DragDrop(object? sender, DragEventArgs e) { if (e.Data != null && e.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); if (files.Length != 1) return; SetPath(files[0]); } } } |
SetPathメソッドを示します。
登録するときにアイコンもいっしょに登録したいものです。ファイルの場合は比較的簡単です。Icon.ExtractAssociatedIconメソッドを使えば、指定したファイルに含まれているイメージのアイコン表現を取得することができるのです。アイコンが取得できたらこれをBitmapに変換してPictureBoxに表示させます。
フォルダの場合はAPI関数を使います。この処理は後述するGetFolderImageメソッドでおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class PanelEx : Panel { public void SetPath(string path) { _path = path; _pictureBox.SizeMode = PictureBoxSizeMode.StretchImage; if (File.Exists(_path)) { Icon? appIcon = Icon.ExtractAssociatedIcon(path); if (appIcon != null) _pictureBox.Image = appIcon.ToBitmap(); } else if (Directory.Exists(path)) _pictureBox.Image = GetFolderImage(); else _pictureBox.Image = null; } } |
フォルダのアイコンを取得する処理を示します。SHGetFileInfo関数を使うとフォルダのアイコンを取得することができます。失敗したときはnullが返されるので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 |
public class PanelEx : Panel { Image? GetFolderImage() { SHFILEINFO shinfo = new SHFILEINFO(); IntPtr hSuccess = SHGetFileInfo("", 0, ref shinfo, (uint)System.Runtime.InteropServices.Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_LARGEICON); if (hSuccess != IntPtr.Zero) { Icon appIcon = Icon.FromHandle(shinfo.hIcon); return appIcon.ToBitmap(); } return null; } // SHGetFileInfo関数 [System.Runtime.InteropServices.DllImport("shell32.dll")] private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags); // SHGetFileInfo関数で使用する構造体 private struct SHFILEINFO { public IntPtr hIcon; public IntPtr iIcon; public uint dwAttributes; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 260)] public string szDisplayName; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 80)] public string szTypeName; }; // SHGetFileInfo関数で使用するフラグ private const uint SHGFI_ICON = 0x100; // アイコン・リソースの取得 private const uint SHGFI_LARGEICON = 0x0; // 大きいアイコン private const uint SHGFI_SMALLICON = 0x1; // 小さいアイコン } |
GetPathメソッドは外部からファイルのパスを取得するときに使います。
1 2 3 4 5 6 7 |
public class PanelEx : Panel { public string GetPath() { return _path; } } |
ランチャーとして機能させる
PictureBoxをクリックしたら登録されているファイルやフォルダを開きます。.NET FrameworkでプログラミングしていたときはSystem.Diagnostics.Process.Start(path)でよかったのですが、.NET Windowsフォームアプリの場合はSystem.Diagnostics.Process.Start(“Explorer.exe”, path)としないとアクセス拒否の例外が発生してしまいます。
もしクリックしたところになにも登録されていない場合はメッセージボックスを表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class PanelEx : Panel { private void PictureBox_Click(object? sender, EventArgs e) { try { //System.Diagnostics.Process.Start(path); // これではアクセス拒否の例外が発生する if(System.IO.Directory.Exists(_path) || System.IO.File.Exists(_path)) System.Diagnostics.Process.Start("Explorer.exe", _path); else MessageBox.Show("該当するファイルまたはフォルダが存在しません"); } catch { MessageBox.Show("例外発生"); } } } |
PictureBoxの上をマウスが移動したときにファイルやフォルダが登録されている場合はそのパスを表示します。またForm1クラスの説明はしていないのでForm1.ShowFilePathメソッドって何だ?と思うかもしれませんが、フォーム上にはファイルパスを表示するためのLabelを設置しています。Form1.ShowFilePathメソッドはそのLabelにパスを表示させます。
1 2 3 4 5 6 7 |
public class PanelEx : Panel { private void PictureBox_MouseMove(object? sender, MouseEventArgs e) { _form.ShowFilePath(_path); } } |
ドラッグアンドドロップで入れ替える
ここからが本番です。ドラッグアンドドロップで登録したパスの順序を入れ替えます。
ドラッグアンドドロップで順序を入れ替えるためには入れ替えたいパスが登録されているPictureBoxをクリックしなければなりません。このときファイルを開く処理と区別するためにマウスボタンがおされてもすぐにドラッグの開始処理をおこないません。マウスボタンをおしたまま一定の距離を移動したときにドラッグ開始と判断します。
マウスボタンがおされたらその座標を記憶します。そしてドラッグが開始されることなくマウスボタンが離された場合はPoint.Emptyを代入します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class PanelEx : Panel { Point _mouseDownPoint = Point.Empty; private void PictureBox_MouseDown2(object? sender, MouseEventArgs e) { //マウスの左ボタンだけが押されている時のみドラッグできるようにする if (Control.MouseButtons == MouseButtons.Left) { // マウスボタンがおされた座標を記憶しておく _mouseDownPoint = new Point(e.X, e.Y); } else _mouseDownPoint = Point.Empty; } private void PictureBox_MouseUp2(object? sender, MouseEventArgs e) { _mouseDownPoint = Point.Empty; } } |
PictureBoxの上でマウスボタンがおされたままX方向またはY方向に10ピクセル以上移動したらドラッグの開始と判断します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PanelEx : Panel { private void PictureBox_MouseMove2(object? sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left && _mouseDownPoint != Point.Empty) { if (Math.Abs(_mouseDownPoint.X - e.X) > 10 || Math.Abs(_mouseDownPoint.Y - e.Y) > 10) { // ドラッグ処理を開始する DragDropEffects dde = DoDragDrop(this, DragDropEffects.All); } } } } |
ドラッグされているときにe.Data.GetDataPresent(typeof(PanelEx))であれば移動のためのドラッグがおこなわれていることになります。この場合はマウスポインタの形状を「移動」の形に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PanelEx : Panel { private void PictureBox_DragOver2(object? sender, DragEventArgs e) { if (e.Data != null && e.Data.GetDataPresent(typeof(PanelEx))) { PanelEx panelEx = (PanelEx)e.Data.GetData(typeof(PanelEx)); if (panelEx != null) e.Effect = DragDropEffects.Move; else e.Effect = DragDropEffects.None; } } } |
ドロップされたときの処理を示します。ドロップされた先になにも登録されていない場合はその位置に移動して移動元の情報は削除します。ドロップされた先に他のデータが登録されている場合はそれ以降のデータをずらします。
データをずらすときの処理ですが、フォーム上に存在するPanelExのパスをすべて取得します。そして移動元のデータを抜き出して移動先のデータがあるところに割り込ませます。そしてそのデータをフォーム上に存在するすべてのPanelExに再度割り当てます。
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 |
public class PanelEx : Panel { private void PictureBox_DragDrop2(object? sender, DragEventArgs e) { if (e.Data != null && e.Data.GetDataPresent(typeof(PanelEx))) { PanelEx panelEx = (PanelEx)e.Data.GetData(typeof(PanelEx)); // Form1.GetPanelList()メソッドはフォーム上に存在するすべてのPanelExオブジェクトを返す List<PanelEx> list = _form.GetPanelList(); PanelEx source = list[panelEx.Index]; PanelEx target = list[this.Index]; if (target._path == "") { // ドロップされた先になにも登録されていない場合 target._pictureBox.Image = source._pictureBox.Image; target._path = source._path; source._path = ""; source._pictureBox.Image = null; } else { // ドロップされた先に他のデータが登録されている場合はそれ以降のデータをずらす List<string> vs = new List<string>(); foreach (var panel in list) vs.Add(panel.GetPath()); string path = vs[source.Index]; vs.RemoveAt(source.Index); vs.Insert(target.Index, path); int index = 0; foreach (var panel in list) { panel.SetPath(vs[index]); index++; } } } } } |
Form1クラスにおける処理
Form1クラスにおける処理ですが、コンストラクタ内で必要な数のPanelExを生成してフォーム上に配置します。ここでは4行6列に配置しています。そしてその下にパスを表示するためのLabelを配置しています。パスが長いと複数行になってしまうことが予想されます。そこでAutoSizeはfalseを設定して文字列が端まで来たら折り返して表示するようにしています。
PanelExクラス内で呼び出していたGetPanelListメソッドとShowFilePathメソッドもここで定義しています。
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 |
public partial class Form1 : Form { List<PanelEx> _panelExs = new List<PanelEx>(); Label _label = new Label(); public Form1() { InitializeComponent(); this.BackColor = Color.Black; // レイアウト的な部分(このあたりはお好みで) int index = 0; int lastX = 0; int lastY = 0; for (int row = 0; row < 4; row++) { int y = 40 + (int)(row * PanelEx.PanelHeight * 1.5); for (int col = 0; col < 6; col++) { PanelEx panelEx = new PanelEx(this, index); index++; this.Controls.Add(panelEx); int x = 20 + (int)(col * PanelEx.PanelWidth * 1.5); panelEx.Location = new Point(x, y); _panelExs.Add(panelEx); lastX = x; } lastY = y; } lastY += (int)(PanelEx.PanelHeight * 1.5); lastX += (int)(PanelEx.PanelWidth * 1.5); this.Controls.Add(_label); _label.AutoSize = false; _label.Location = new Point(20, lastY); _label.Size = new Size(lastX - 20 - (int)(PanelEx.PanelWidth * 0.5), 48); _label.BorderStyle = BorderStyle.FixedSingle; _label.ForeColor = Color.White; this.Size = new Size(330, 330); this.FormBorderStyle = FormBorderStyle.Fixed3D; this.MaximizeBox = false; InitNotifyIcon(); // あとで使う } void InitNotifyIcon() { } public List<PanelEx> GetPanelList() { return _panelExs; } public void ShowFilePath(string path) { _label.Text = path; } } |
登録データの保存と読み出し
それからデータを保存する処理と読み出す処理、右上の×ボタンを押しても終了しない(タスクトレーのなかに入る)ようにするためのコードも示します。
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 |
public partial class Form1 : Form { private void saveToolStripMenuItem_Click(object sender, EventArgs e) { List<string> vs = new List<string>(); foreach (var panel in _panelExs) vs.Add(panel.GetPath()); var xml = new System.Xml.Serialization.XmlSerializer(typeof(List<string>)); SaveFileDialog save = new SaveFileDialog(); save.Filter = "ランチャー用ファイル(*.dat)|*.dat"; if (save.ShowDialog() == DialogResult.OK) { var sw = new System.IO.StreamWriter(save.FileName); xml.Serialize(sw, vs); sw.Close(); } save.Dispose(); } private void loadToolStripMenuItem_Click(object sender, EventArgs e) { var xml = new System.Xml.Serialization.XmlSerializer(typeof(List<string>)); OpenFileDialog open = new OpenFileDialog(); open.Filter = "ランチャー用ファイル(*.dat)|*.dat"; if (open.ShowDialog() == DialogResult.OK) { List<string>? paths = null; try { StreamReader sr = new StreamReader(open.FileName); paths = (List<string>?)xml.Deserialize(sr); sr.Close(); } catch { } if (paths == null) { MessageBox.Show("ファイルのデータを読み取れません", "エラー"); return; } int i = 0; foreach (var panel in _panelExs) { panel.SetPath(paths[i]); i++; } } open.Dispose(); } } |
常駐アプリとして使う
×ボタンをクリックしたらタスクトレーのなかに入るようにするには以下のようにします。
OnFormClosingをオーバーライドすることで終了しないアプリをつくることができます。ここではタスクトレーのなかにアイコンをつねに表示するようにして×ボタンが押されたらフォームを非表示にしています。IsEndフラグがfalseの場合、フォームが非表示になるだけで終了することはありません。
これでは本当に終了させたいときに終了できなくなるので終了するために別の手段を用意します。メニューで終了を選択するとIsEndフラグがtrueになってApplication.Exit()が呼び出されます。今度はIsEndフラグがtrueなのでアプリケーションを終了させることができます。
それからフォームが非表示になっている場合はメニューで終了を選択することができないので、フォームを再表示させるための手段も用意します。タスクトレイに表示されている通知アイコンがクリックされたときは非表示になっているフォームを表示させます。
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 |
public partial class Form1 : Form { NotifyIcon notify = new NotifyIcon(); void InitNotifyIcon() { notify.Icon = Properties.Resources.Icon1; // 適当にアイコンをつくってリソースに追加しておく notify.Visible = true; notify.MouseDown += Notify_MouseDown; } // ×ボタンをクリックした場合は終了しないでフォームを非表示にする bool IsEnd = false; protected override void OnFormClosing(FormClosingEventArgs e) { if (!IsEnd) { e.Cancel = true; this.Visible = false; return; } base.OnFormClosing(e); } // タスクトレイの通知アイコンがクリックされたら非表示になっているフォームを表示させる private void NotifyIcon1_MouseClick(object sender, MouseEventArgs e) { this.Visible = true; } private void EndToolStripMenuItem_Click(object sender, EventArgs e) { notify.Visible = false; IsEnd = true; Application.Exit(); } } |
いつもありがとうございます。楽しく見てます。
これを日々使うアプリとして活用して行きたいと思います。
notify.MouseDown += Notify_MouseDown; が何を意味しているのか 非常に悩みました。
この一行を notify.ContextMenuStrip = contextMenuStrip1; に変えたところ上手くいきました。 VisualStudio2022 使用上での話です。 色々Webサイトで調べても判らず、考えた挙句、理屈的にはこのようにするしかないだろうと思って書いた結果です。当方にとっては一つの発見です。
Excel VBA, VBS, PowerShell, JavaScript を見様見真似でしか使用したことがないのですが、今般、何故か Visual studio 2020 Community が無料ということで目に留まり、興味深々で使用してみようかと思い、教材として、貴殿のこのWebページを参考にさせてもらいました。非常に参考になり、やりたかったら這いずり上がって来いというような内容で、少し鍛えられました。ありがとうございました。
申し訳ありません。notify.MouseDown += Notify_MouseDown;の意味ですが、タスクトレイに表示されているアイコンをクリックしたらNotify_MouseDownを実行せよという意味です。ところが肝心のNotify_MouseDownをこのページに書いていませんでした。
タスクトレイに表示されているアイコンをクリックしたらフォームが非表示になっている場合は表示させよという意味です。記事も加筆修正しておきます。
notify.MouseDown += Notify_MouseDown; ではなく notify.MouseDown += NotifyIcon1_MouseClick; でしたね。 納得しました。 notify.ContextMenuStrip = contextMenuStrip1; も必要ですよね。