ごまなつ Blog

楽しく働ける世界を目指して

【C#】ListViewのアイテム選択が外れる

ListViewを使っており、ContextMenustripを使っていました。選択されているItemが空白の場合と空白でない場合でContextMenustripの内容を変更していました。

アイテム選択が外れる場合

ListViewは、最終番号の下に空白が存在します。スクロールバーを表示するほどのアイテム数だったとしても、狭い範囲ですが空白が存在し、ここをクリックするとListViewのアイテムを何も選択しない状態になります。また、横にListViewを広げたときなど、アイテムの内容が存在しない部分をクリックしてもListViewのアイテムを何も選択しない状態になります。

MouseUpではイベントハンドラが発生するが、MouseClickでは発生しない

このアイテム選択が外れる領域では、クリックしてもListView.MouseClickは発生しません。しかし、MouseDown、MouseUpは発生します。今回の場合は、クリックした後のマウスの座標で処理したかったため、MouseUpを用います。

クリックした場所に一番近いアイテムを選択する

listView.InsertionMark.NearestIndex()では、指定したポイントに最も近い項目のインデックスを取得します。ポイントは、座標です。クリックした座標に最も近いListViewのインデックスを取得できるので、そこを選択状態にします。

private void listView_MouseUp(object sender, MouseEventArgs e)
        {
            // Retrieve the index of the item closest to the mouse pointer.
            int targetIndex = listView.InsertionMark.NearestIndex(new Point(e.X, e.Y));
            if (listView.SelectedItems.Count < 1)
            {
                listView.Items[targetIndex].Selected = true;
                listView.Items[targetIndex].Focused = true;
            }

こうすることで、下の空白をクリックすると最終番号が選択され、横の空白部分をクリックするとその場所に一番近いアイテムが選択されます。

座標系で注意すること

C#での座標系には以下の2種類があります。

  • ディスプレイの左上端が(0,0)
  • コントロールの左上端が(0,0)

使用しているライブラリがどちらの座標を用いているか確認しましょう。思っている座標とずれます。今回の場合は、コントロールの左上端が(0,0)なのでイベントハンドラが取得したクリックした座標をそのまま代入しています。ディスプレイの左上端が(0,0)の場合は、

Point targetPoint =  ListView.PointToClient(new Point(e.X, e.Y));

としてコントロールの左上端が(0,0)の座標に変換してください。

【outlook】定期的にメールを送信したい

業務の中で、同じ内容のメールを定期的に送信したいことがあると思います。そのようなときに、完全に自動とまではいきませんでしたが予定のアラームをクリックすることでメールを送信することができました。

参考サイト

https://docs.microsoft.com/ja-jp/outlook/troubleshoot/user-interface/create-recurring-emaildocs.microsoft.com

方法

outlookは、内部でvbsのマクロを走らせることができます。そこで、メール内容を入力した状態で送信フォームを開く、もしくは送信まで行うことができます。

メールを送信するフォームの作成

  1. まず、タブに開発を表示します。リボンの何もないところを右クリックして、リボンのユーザ設定を開きます。

右側のメインタブに、開発があるのでチェックを入れ、OKをクリックします。すると、メインタブに開発が表示されます。

  1. タスクを開き、[新しいタスク]をクリックします。

  2. タスク作成画面で[開発]の[このフォームのデザイン]をクリックし、コードの表示をクリックします。

  3. 開いたスクリプトエディター画面で、以下を入力します。宛先、CC、件名、本文は適宜変更してください。

Sub Item_PropertyChange(ByVal Name)    
    Select Case Name     
        Case "Status"
            if Item.Status = 2 then '2 = Completed
                Set NewItem = Application.CreateItem(0)
                NewItem.To = "宛先"
                NewItem.Cc =   "CC"
                NewItem.Recipients.ResolveAll
                NewItem.Subject = "件名"
                NewItem.Body = "本文"
                NewItem.Display
            End IF
        Case Else
    End Select
End Sub
  1. スクリプトエディターを閉じ、[フォーム]の[フォームの発行]をクリックします。

  2. [フォルダーの場所]で[タスク]をクリックし、[表示名][フォーム名]に好きな名前を入力して[発行]をクリックします。

  3. タスク作成画面を閉じます。変更は保存しません。

ここで作成したものは、[開発]の[フォームの選択]で選択することができ、上のコードが実行されます。その結果、設定した宛先、CC、件名、本文が入力されたメール送信画面が開きます。

定期的にするため予定のアラームと紐づける

  1. 先ほど作成したタスク作成画面を再び開きます。[開発]の[フォームの選択]か、[新しいアイテム]の[その他のアイテム]の[フォームの選択]で先ほど作成したタスクを開きます。
  2. タスクの件名、期限を設定します。[アラーム]チェックボックスをオンにします。また、定期的なアイテムをクリックし、アラームの頻度を設定し保存して閉じるをクリックします。 これで完成です。

実際の運用

タスクで設定した頻度で期限が来たらアラームが表示されます。そのアイテムを開き[進捗状況を完了にする]をクリックすると上のマクロが実行されて送信画面が開きます。 メール送信まで行いたい場合は、マクロのNewItem.Displayの後にNewItem.Sendを追加することでメール送信まで行うことができます。

【C#】ファイル名は大文字小文字を区別しない

File.Exists()で存在確認をしたり、ファイルパスを指定してファイル保存をすることがあると思います。その時、ファイル名は大文字小文字を区別しません。

どういうことか?

例えば、デスクトップに「FILE」というファイルがあるとします。File.Exists()で存在確認して上書き確認をしようとするとき、「File」「file」「fILE」のファイル名でもtrueが返ってきます。

注意すべき点になるかもしれません。ToUpper()やToLower()を用いて揃えることも考えられますね。

どうしても区別したい場合

Directory.EnumerateDirectoriesを用いましょう。指定したフォルダに存在するフォルダ名一覧を取得できるものです。多段に呼んでいくことで正確なパスになります。

【C#】ReadOnlyのDataGridViewでも、ComboboxColumnやButtonColumnは押せる(例外が出る)

内容を表示するだけで、操作させたくないDataGridViewを表示するとします。今回は、DataGridViewComboboxColumnを追加していました。

参照だけにしたいならReadOnly

参照だけにしたいならReadOnly=trueとすれば読み取り専用になります。が、DataGridViewComboboxColumnは押せてしまいます。DataGridViewの上にComboboxが載っているからでしょうか?DataGridViewはReadOnlyになっていても、その上にあるものはReadOnlyにならないのだと考えます。

Enabled=falseにしてみよう

次にEnabled=falseにすることで何とかしようと考えました。ですが、これではデータ列が多い場合に、スクロールバーを出していましたがスクロールバーが機能しなくなり画面外のデータが見れなくなります。

解決策

ReadOnly=trueにしたうえで、CellEnterのイベントハンドラでComboboxColumnが押された瞬間にreturnする。

これでうまくいきました。

【C#】filestreamは既存ファイルに上書きできないのか?

zipファイルを読み込み、中身のxmlファイルの内容を編集してzipファイルを保存するソフトを作っています。その中で、「多重起動できて、同じファイルを開いたときは2つ目以降は編集できないように、異なるファイルは編集できるように」という要望があったため、filestreamを用いて開いているファイルの判断を行い、filestreamで保存するようにしました。

削除が保存できていない?

新しいxmlファイルを作る処理があり、その処理をした後一旦保存。その後削除してファイルを確認すると、削除したはずのファイルが削除されていません。

パスワードが変更される?

今回のzipファイルにはパスワードをかけていたのですが、そのパスワードが変更されていました。

奇怪な動作の理由

filestreamは、上書きをする処理がStreamWriterとは異なるようです。filestreamとして確保した長さより短いデータだと、既存のデータが残るようです。よって、StreamWriterを用いたいのですが、ファイルロックを使いたいのであればfilestreamを用いる必要があります。

解決策

fileStream.SetLength(0);

これを使うと正常にできるようになりました。filestreamの長さを強制的に0に設定することによって、常にfilestreamより長いデータである状況にしています。ほかの良い方法があるかもしれないので、調べていきます。

【C#】DataGridViewで、チェックボックスの列を一括で変更したい

DataGridViewでチェックボックスの列を一括で変更したいときに苦しんだメモです。

DataGridViewの値を取得したいとき

DataGridViewには、3つのValueが存在します。

  • Value
  • FormattedValue
  • EdittedFormattedValue

Datagridviewは、実は表示用のDataGridViewが上にあり、その下に実際のDataGridViewが存在しています。つまり、自分で値を変更しても表示用のDataGridViewしか値が変わっていません。 上記のValueは、実際のDataGridViewの値を参照するため、Valueだと変更されていません。 FormattedValueが表示用のDataGridViewの値を参照するため、変更を感知するときはFormattedValueになります。 ちなみに、EdittedFormattedValueは、現在の書式指定済みの値を取得します。FormattedValueとの違いがまだ分かっていません。

DataGridViewのチェックボックスのColumn

DataGridViewは、Columnに型を設定できます。Columnにboolを設定すると、チェックボックスのColumnになります。上記の仕様の通り、変更すると表示用の値しか変わりません。他のセルを選択することによってはじめて、Valueが変更されます。これを解決するために、CurrentCellDirtyStateChanged()を使いました。

private void DataGridView_CurrentCellDirtyStateChanged(object sender, EventArgs e)
        {
            var dgv = (DataGridView)sender;
            if (dgv.IsCurrentCellDirty)
            {
                dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
            }
        }

これで、チェックを入れた瞬間にValueも変更されます。

一括でチェックを入れたい

DataGridViewのセルを範囲選択して、チェックを入れられるようにしようと考えました。 DataGridViewのチェックボックスに設定したColumnに右クリックメニューを追加し、一括チェック操作を書きました。今回チェックを入れるColumnは、indexは1です。

 foreach (DataGridViewCell a in dgv.SelectedCells)
                        {
                            if (a.ColumnIndex == 1)
                            {
                                a.Value = true;                               
                            }
                        }

セルの選択は複数Columnにわたることもできるので、回避のためColumnIndexを指定しています。表示ではすべてチェックが入ったのですが、実はValueが変更されていない場所が1か所ありました。*右クリックしたセルは、Valueが変更されていなかったのです。もっと調べると、一つずつ変更した場合はCurrentCellDirtyStateChangedが発火していますが、一括変更の場合は発火していませんでした。

解決法

チェックボックスの値を表示用が変更された瞬間にValueを変更するためには、EndEdit()を使います。これをすることで今回の問題は解決しました。

まとめ

DataGridViewのvalueの仕様から起こる挙動をチェックボックスの一括値変更から解説しました。DataGridViewの仕様を学んだので、次に生かしていこうと思います。

【C#】ListViewのSelectedIndex変更禁止に立ちふさがる壁

C#のListViewにて、ある条件のときは選択しているアイテムを変更させないようにしたいことがありました。アイテム変更なのでitemSelectionChangedを用いました。選択しているアイテムの変更は、

  1. 選択しているアイテムの選択が外れる
  2. 何も選択されていない状態
  3. クリックしたアイテムが選択される

という順番になっています。

避けるべきこと

上記の2においては、何も選択されていない状態なのでインデックスが-1になります。また、3において選択操作をキャンセルすると何も選択していない状態になります。これを回避する必要があります。

使えるもの

ListViewItem型の変数を用意して、itemSelectionChangedの中で使うと選択しているアイテムになります。e.itemは変更先のアイテムです。また、MouseUpイベントハンドラ(マウスクリックを離したとき)を用いて選択しているアイテムにインデックスを戻せば、アイテムを変更させないことができます。

問題

この考えに従って普通に書くと、itemSelectionChangedの中で選択しているアイテムを変更することになるので、処理の順番などから思わぬ動作になることがあります。私も、選択が消えることが多くありました。よって、Task.Runを用いてアイテム変更動作のタイミングをずらしました。

追加要望

ここで、右クリックメニューを追加してほしいという要望が来ました。上記の動作を持ったまま行うと、選択しているアイテムに選択を戻すため、選択しているアイテムが選択状態になる(背景青に白抜きの文字になる)が、右クリックメニューはクリック位置に出るという違和感満載の見た目になりました。なにか良い方法があるとよいのですが・・・・・・