2016-03-25

PowerShell ドラッグ&ドロップでListViewの項目を移動させる

PowerShellでは、ユーザーの好みに合わせて、独自にフォームを作成することができます。

今回は以前ご紹介したリストビューを少し発展させて、ドラッグ&ドロップで項目を移動できるようにしたいと思います。

リストビューについて詳しく知りたい方はPowerShellでユーザーフォームを作る - リストビュー編 -をご参照ください。

<今回の完成品>


早速スクリプトを記述していきます。
少々長いですが、ご了承ください。

 
# ListView ドラッグ&ドロップで項目を移動させる

Add-Type -AssemblyName System.Windows.Forms

$form = New-Object System.Windows.Forms.Form
$form.Size = "400,230"
$form.StartPosition = "CenterScreen"
$form.Text = "リストビュー"

# ListView
$View = New-Object System.Windows.Forms.ListView
$View.Location = "10,10"
$View.Size = "360,130"
$View.View = "Details"
$View.GridLines = $True
$View.AllowDrop = $True
$View.MultiSelect = $False # 複数選択:不可

#アイテムドラッグイベント
$DragItem = {
    $View.DoDragDrop($_.Item, "Move" )
}
$View.Add_ItemDrag($DragItem)

#ドラッグエンターイベント
$Enter = {
    $_.Effect = "Move"
    $script:name = $View.SelectedItems.SubItems[0].Text #ファイル名
    $script:page = $View.SelectedItems.SubItems[1].Text #ページ数
    $script:size = $View.SelectedItems.SubItems[2].Text #サイズ
    $script:index = $View.SelectedItems.Index #インデックス番号
}
$View.Add_DragEnter($Enter)

#ドラッグオーバーイベント
$Over = {
    #マウスポインタの座標から、どの項目の上に移動しているか判断する
    $where = New-Object System.Drawing.Point($_.X , $_.Y)
    $point = $View.PointToClient($where)
    $OverItem = $View.GetItemAt($point.X,$point.Y)

    #ドラッグオーバーしている項目を選択状態にする。
    $select = $View.Items.IndexOf($OverItem)
    
    IF ( -Not ( $select -eq "-1" ) )
    {
        $OverItem.Selected = $True
    }
    
    #インデックス番号を一時保管
    $script:index2 = $select
}
$View.Add_DragOver($Over)

#ドラッグドロップイベント
$Drop = {
    $Element_Name = $name
    $Element_Page = $page
    $Element_Size = $size
    $Element_Index = [int32]$index

    #移動先のインデックス番号から分岐
    IF ( $index2 -eq "-1" ) #「-1」の場合は最後尾に設定
    {
        $After_Index = $View.Items.Count
    }else{ #「-1」以外の場合は、選択した項目のインデックスに設定
        $After_Index = $View.SelectedItems.Index
    }

    # 項目の移動を実行
    $Item = New-Object System.Windows.Forms.ListViewItem("$Element_Name") # ファイル名
    [void]$Item.SubItems.Add("$Element_Page") # ページ数
    [void]$Item.SubItems.Add("$Element_Size") # サイズ
    [void]$View.Items.Insert($After_Index,$Item)

    #移動元の項目 > 移動先の項目 の場合は、移動元インデックスに+1する
    IF ( $Element_Index -gt $After_Index ) { $Element_Index += 1 }
    
    #元の項目を削除
    $View.Items[$Element_Index].Remove()

    #移動した項目を選択する
    $Item.Selected = $True
}
$View.Add_DragDrop($Drop)

# リストビューにヘッダーを追加
[void]$View.Columns.Add("ファイル名",150)
[void]$View.Columns.Add("ページ数",100)
[void]$View.Columns.Add("サイズ",100)

#項目を追加======================================================
$Item1 = New-Object System.Windows.Forms.ListViewItem("ファイルA.pdf") 
[void]$Item1.SubItems.Add("3")
[void]$Item1.SubItems.Add("0.26")
[void]$View.Items.Add($Item1)

$Item2 = New-Object System.Windows.Forms.ListViewItem("ファイルBB.pdf")
[void]$Item2.SubItems.Add("50")
[void]$Item2.SubItems.Add("6.90")
[void]$View.Items.Add($Item2)

$Item3 = New-Object System.Windows.Forms.ListViewItem("ファイルCCC.pdf")
[void]$Item3.SubItems.Add("23")
[void]$Item3.SubItems.Add("1.50")
[void]$View.Items.Add($Item3)

$Item4 = New-Object System.Windows.Forms.ListViewItem("ファイルDDDD.pdf")
[void]$Item4.SubItems.Add("100")
[void]$Item4.SubItems.Add("5.03")
[void]$View.Items.Add($Item4)
#============================================================

# 閉じるボタン
$Button = New-Object System.Windows.Forms.Button
$Button.Location = "290,150"
$Button.size = "80,30"
$Button.text  = "閉じる"
$Button.DialogResult = [System.Windows.Forms.DialogResult]::Cancel

$form.Controls.AddRange(@($View,$Button))
$form.Showdialog()

上記内容をコピーし、PowerShell ISEに貼り付け後、実行すると上の画像と同じものが表示されるはずです。

続いて解説をしていきます。

※過去の記事で解説した部分については割愛致します。
 内容をご確認いただきたい場合は【ユーザーフォーム - 基礎編 -】をご覧ください。

*****解説*****************************************************************************

項目の移動は、簡単なようで実は結構複雑です。
手順としてはおおまかに次の4イベントを実施しなくてはいけません。

  1.アイテムドラッグイベント
  2.ドラッグエンターイベント
  3.ドラッグオーバーイベント
  4.ドラッグドロップイベント

特に3.ドラッグオーバーイベントでは、「マウスポインタの座標から、どの項目の上にポインタがあるのかを判断する」といった処理が必要となります。
(この辺のサンプルがほとんど無く、当時は結構苦労しました)
# アイテムドラッグイベント
$DragItem = {
    $View.DoDragDrop($_.Item, "Move" )
}
$View.Add_ItemDrag($DragItem)
まずはアイテムドラッグイベントです。
発生タイミングは「項目をドラッグした時」です。
ここではDoDragDropメソッドを用いてドラッグ&ドロップの操作を開始し、ItemDragEventArgsクラスのプロパティをItemとして、DragDropEffect列挙体にMoveを指定しています。

これを要約すると、「さぁ、これからみんなで旅行にいこう!お弁当(アイテムのデータ)を持って!手段はそうだな・・・車(コピー)・・・いや、電車(移動)にしよう!」
こんなイメージでしょうか・・・。
# ドラッグエンターイベント
$Enter = {
    $_.Effect = "Move"
    $script:name = $View.SelectedItems.SubItems[0].Text #ファイル名
    $script:page = $View.SelectedItems.SubItems[1].Text #ページ数
    $script:size = $View.SelectedItems.SubItems[2].Text #サイズ
    $script:index = $View.SelectedItems.Index #インデックス番号
}
$View.Add_DragEnter($Enter)
続いてドラッグエンターイベントです。
発生タイミングは「ドラッグ状態でマウスポインタがリストビュー内に入ってきた時」です。
ここでは、現在選択中の(=ドラッグ中の)項目について、各Text情報を取得し、変数に格納しています。
変数のスコープを「script」にするのを忘れないようにしてください。
そうしないと、ドラッグエンターイベントが終わった瞬間に、その変数は参照できなくなります。

この処理を要約すると「出発の準備はできたか?!持っていくものは"ファイル名"と"ページ数"と"サイズ"と"インデックス番号"だぞ!」
#ドラッグオーバーイベント
$Over = {
    #マウスポインタの座標から、どの項目の上に移動しているか判断する
    $where = New-Object System.Drawing.Point($_.X , $_.Y)
    $point = $View.PointToClient($where)
    $OverItem = $View.GetItemAt($point.X,$point.Y)

    #ドラッグオーバーしている項目を選択状態にする。
    $select = $View.Items.IndexOf($OverItem)
    
    IF ( -Not ( $select -eq "-1" ) )
    {
        $OverItem.Selected = $True
    }
    
    #インデックス番号を一時保管
    $script:index2 = $select
}
$View.Add_DragOver($Over)
次にドラッグオーバーイベントです。
発生タイミングは「ドラッグ中のマウスポインタがリストビューの上にある時」です。
ここでは、まずマウスポインタの画面上の座標を取得し(変数where)、その座標をリストビュー内での座標(クライアント座標)に当てはめます。(変数point)
そして、クライアント座標に合致するアイテムを取得します。(変数OverItem)
その後、オーバーされているアイテムのインデックス番号を取得し、その番号が「-1」以外の場合はそのアイテムを選択している状態にします。
この「-1」とは「アイテムが何もない場所」であることを示しています。
最後に、オーバーされているアイテムのインデックス番号を変数index2に格納しています。

この処理を要約すると「今いる地点は東経***北緯***だから、日本地図でいうとこの辺りだな。つまり、○○施設があるところだ!よし、到着記念に色を塗っておこう!将来のために、その地点も覚えておくんだぞ!」
# ドラッグドロップイベント
$Drop = {
    $Element_Name = $name
    $Element_Page = $page
    $Element_Size = $size
    $Element_Index = [int32]$index

    #移動先のインデックス番号から分岐
    IF ( $index2 -eq "-1" ) #「-1」の場合は最後尾に設定
    {
        $After_Index = $View.Items.Count
    }else{ #「-1」以外の場合は、選択した項目のインデックスに設定
        $After_Index = $View.SelectedItems.Index
    }

    # 項目の移動を実行
    $Item = New-Object System.Windows.Forms.ListViewItem("$Element_Name") # ファイル名
    [void]$Item.SubItems.Add("$Element_Page") # ページ数
    [void]$Item.SubItems.Add("$Element_Size") # サイズ
    [void]$View.Items.Insert($After_Index,$Item)

    #移動元の項目 > 移動先の項目 の場合は、移動元インデックスに+1する
    IF ( $Element_Index -gt $After_Index ) { $Element_Index += 1 }
    
    #元の項目を削除
    $View.Items[$Element_Index].Remove()

    #移動した項目を選択する
    $Item.Selected = $True
}
$View.Add_DragDrop($Drop)
いよいよドラッグドロップイベントです。
発生タイミングは「アイテムをドロップした時」です。
まずドラッグエンターイベントの時に取得していたものを改めて変数に入れ直しています。(必須ではありません。自身が分かりやすくするためにしたことです)
続いて、移動先のインデックス番号を確定させます。
ドラッグオーバーイベント時の、オーバされていたアイテムのインデックス番号によって分岐させています。
移動先を確定後、リストビューにドラッグしていたアイテムを挿入します。
この時点では、移動前のアイテムが残ったままになってしまいますので、移動前のアイテムを削除する処理を入れています。
この時、移動前のインデックス番号が移動先のインデックス番号よりも大きい場合はインデックス番号がひとつズレてしまうことに注意してください。
(インデックス3のアイテムをドラッグし、インデックス2の部分に挿入したとき、元のアイテムのインデックス番号は3から4になります)
最後に、移動後のアイテムを選択状態にして完了です。

***************************************************************************************************
解説は以上となります。

最後まで読んでいただき、誠にありがとうございました!

GUI上で見るとなんてことない動作なのですが、その裏ではこんなに複雑な処理になっています。

手動での並び替えができるようになると、操作の幅がグッと広がります。
これまでに作成されたリストビューがあるのであれば、今回の機能を追加することをご検討してみてはいかがでしょうか。
==================================================================
本投稿に関する疑問や質問には可能な限りお答えさせていただきます。
お気軽にコメントやメールをお送りください。
(リクエストも歓迎します)
メール:tkk-powershell@gmail.com
また、間違いのご指摘・アドバイス等も歓迎いたします。
==================================================================
この記事が参考になりましたら、シェア・フォロー・おすすめしていただけると励みになります
==================================================================
スポンサーリンク


2 件のコメント:

  1. 素晴らしく解りやすい内容をありがとうございます。
    PowerShell初心者にはとても助かります。
    リスト内にドラッグアンドドロップしたファイル名を$Listbox配列に入っているだろうですが、先ずはリストの上から順に印刷させたいと思っています。
    印刷ボタンは作れても、forEachで順に印刷させるヒントはございませんでしょうか?

    返信削除
    返信
    1. Unknown様
      コメントありがとうございます。
      また返信が遅くなり申し訳ありません。

      ご質問についてですが、リストビューにファイルパス項目が存在しており、それを上から順に取得したいということでしょうか?

      それでよろしければ、下記のボタンを新たに追加し、ファイルパスの入っているインデックス番号を指定することで可能です。

      #ボタン
      $b = New-Object System.Windows.Forms.Button
      $b.Location = "10,150"
      $b.size = "80,30"
      $b.Text = "button"
      $b.Add_Click({
      for($i=0;$i -lt $View.Items.Count;$i++){
      Write-Host($View.Items[$i].SubItems[0].Text)
      }
      })

      上記の内容を「# 閉じるボタン」の上に追加してください。
      そして、「SubItems[0]」の部分の「0」をファイルパスが入力されているインデックス番号に書き換えてください。

      これで、リストビューの項目を上から順に取得できます。

      ご希望の回答になっているでしょうか?
      検討のほどよろしくお願いいたします。

      削除

疑問・質問・リクエスト お気軽にどうぞ (^O^)/