7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiitanがほしい人の一人アドカレAdvent Calendar 2024

Day 4

Windows FormsのDataGridViewがイマイチすぎるので少しはマシに使える方法を考える

Posted at

読み飛ばすのが理想

ぼっちアドカレ4日目の限界派遣SESです。
書くこと思いつかないなーとか思いつつQiitanのぬいぐるみをもらうために今日もない知識を引き出していこうと思います。

今日までほとんどWindowsFormsの悪口しか言っていない気がしますが本日もWindowsFormsについて書いていきます。

DataGridViewにデータを設定するのがだるすぎる

DataGridViewとはFormアプリ上で表形式のデータを表示するために利用できるコントロールです。
よくDataTableなどをDataSourceに指定することで名称を使ってアクセスできるようになることはよく知られていると思います。

とりあえずフォームにてきとうにDataGirdViewを作成して以下のようにてきとうなデータを生成して与えてやります。

public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        var dataTable = new DataTable();

        // カラムに名称をつける
        dataTable.Columns.Add("Column1");
        dataTable.Columns.Add("Column2");
        dataTable.Columns.Add("Column3");

        // てきとうなデータをつっこむ
        foreach (var i in Enumerable.Range(0, 10))
        {
            dataTable.Rows.Add(i, i*2, i*3);
        }

        dataGridView1.DataSource = dataTable;
    }
}

すると以下のようにカラム名が指定された状態でデータが表示されます。

image.png

データを取り出す操作を取り出すのがめんどくさい

先ほどの方法でDataTableから選択行のデータを取り出すという処理を考えてみましょう。

以下の画面のように行を選択するとテキストボックスにデータが入るといった処理を実現してみます。

image.png

コードは以下のように選択行を取り出してDataRowオブジェクトを頑張って獲得することで取得できます。

private void dataGridView1_SelectionChanged(object sender, EventArgs e)
{
    var dataGridView = (DataGridView)sender;

    // 選択行が0個だったら処理終了
    if (dataGridView.SelectedRows.Count == 0)
        return;

    // データRowを取り出す
    var row = (dataGridView.SelectedRows[0].DataBoundItem as DataRowView)?.Row;
    if (row == null)
        return;

    textBox1.Text = $"{row["Column1"]}, {row["Column2"]}, {row["Column3"]}";
}

ね?ひどいでしょう?
rowを取り出すための処理がまるで呪文ですし、結局row型はDataTable型なのでカラム名を指定しなければ取り出すことができません。

また、DataTableでは各値はobject型で渡されるので、キャストするなりの操作が必要だったりと、かなり面倒なことが多いです。

エンティティフレームワークで生成されたDataTableを継承した型であれば、その型にキャストすることでプロパティを使ってアクセスできるようになりますが、GUIで作成された自動生成コードにうんざりしているので使いたくありません。

MODELを定義してデータとして割り当てる

そもそもデータを与える時はMODELを配列形式のデータで渡せるほうが嬉しいですよね。

てきとうな以下のようなModelクラスを定義してあげます。

public class DataModel
{
    public required string Column1 { get; set; }
    public required int Column2 { get; set; }
    public required double Column3 { get; set; }
}

そしてモデルを使って以下のように先ほどのフォームのクラスを書き直します。
ここではDataSourceとして渡すデータがListになっています。

public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        // てきとーなデータを10個つくる
        var dataList = Enumerable.Range(0, 10)
            .Select(x => new DataModel {
                Column1 = $"{x}歳",
                Column2 = x,
                Column3 = x * 1.5,
            })
            .ToList();

        dataGridView1.DataSource = dataList;
    }

    private void dataGridView1_SelectionChanged(object sender, EventArgs e)
    {
        var dataGridView = (DataGridView)sender;

        // 選択行が0個だったら処理終了
        if (dataGridView.SelectedRows.Count == 0)
            return;

        // データをModelとして取り出す
        var row = dataGridView.SelectedRows[0].DataBoundItem as DataModel;
        if (row == null)
            return;

        textBox1.Text = $"{row.Column1}, {row.Column2}, {row.Column3}";
    }
}

以下のように同様の操作が可能になります。

でも、カラム名は直接プロパティ名が使われてしまうため、それもイマイチですよね。
できれば論理名と物理名はわけておきたいです。

image.png

カラムに論理名をつける

これが結構面倒です。

DataTableを利用した例でもそうですが、DataGridViewではカラム名を指定しない限り、プロパティ名がカラム名として利用されます。

また、先ほどの手法ではプロパティから自動でカラムが生成されるため、非表示パラメーターといったものが設定できません。

以下のようにカラムを生成することで、自動割り当てではないカラムを生成することができます。

private void InitDataGirdViewColumns()
{
    // カラムが自動生成されないようにする
    dataGridView1.AutoGenerateColumns = false;

    // カラムを生成する
    dataGridView1.Columns.AddRange([
        new DataGridViewTextBoxColumn
        {
            Name = "ねんれい",
            DataPropertyName = "Column1",
        },
        new DataGridViewTextBoxColumn
        {
            Name = "しんちょう(m)",
            DataPropertyName = "Column2",
        },
        new DataGridViewTextBoxColumn
        {
            Name = "まめを食べる数",
            DataPropertyName = "Column3",
        }
    ]);
}

でも結局これだとカラム名が必要になるし、Modelのプロパティ名を変更した際などDataPropertyNameなども変更しなくてはならないので全然DRYじゃなくていやな気分になります。

Modelの中でDataGridViewの情報を完結させたい

Modelの中だけでDataGridViewで表示される情報をすべて表現できればいい感じですよね。

なので以下のようなAttributeを定義します。

// プロパティのみに付与できる用にするAttribute
[AttributeUsage(AttributeTargets.Property)]
public class DataGirdViewPropAttribute: Attribute
{
    public string LogicName { get; } // 論理名
    public bool Visible { get; } // 可視性
    
    public DataGirdViewPropAttribute(string logicName, bool visible = true)
    {
        LogicName = logicName;
        Visible = visible;
    }
}

これをモデルのプロパティへとセットします。
論理目と物理名がセットになっていていい感じに見えます。

public class DataModel
{
    [DataGirdViewProp("ねんれい", visible:false)] // 年齢は非表示項目とした
    public required string Column1 { get; set; }
    [DataGirdViewProp("しんちょう(m)")]
    public required int Column2 { get; set; }
    [DataGirdViewProp("まめを食べる数")]
    public required double Column3 { get; set; }
}

DataGridViewには拡張メソッドを定義して、型情報からプロパティ情報とカスタム属性を取得し、それらをカラム情報をして追加します。

public static void SetColumns(this DataGridView dataGridView, Type modelType)
{
    // プロパティとカスタム属性を取得
    var propAndAttrs = modelType.GetProperties()
        .Select(prop => (prop, attr: prop.GetCustomAttribute<DataGirdViewPropAttribute>()))
        .Where(x => x.attr != null);

    foreach(var (prop, attr) in propAndAttrs)
    {
        dataGridView.Columns.Add(
            new DataGridViewTextBoxColumn
            {
                DataPropertyName = prop.Name, // プロパティ名を設定
                Name = attr!.LogicName, // 論理名を設定
                Visible = attr!.Visible, // 可視性を設定
            }    
        );
    }
}

最後にSetColumnsメソッドを呼び出すことで完成です。

private void InitDataGirdViewColumns()
{
    // カラムが自動生成されないようにする
    dataGridView1.AutoGenerateColumns = false;

    // カラムを生成する
    dataGridView1.SetColumns(typeof(DataModel));
}

これで論理名のカラムを表示しながら、年齢という重要な個人情報を隠すことができました。

image.png

まとめ

やっぱりレガシーですよねー。アドカレが始まってからずっと文句しか書いていない気がします。
それでも少しでも扱いやすくするために色々考察をするのは楽しいです。
また、今回の方法では行のソートができないという問題がありますが、ソート可能なBindingListを作ることでソート可能にしている方も見受けられます。

また、「こんなんライブラリ使えば余裕やで余裕」など教えていただけると幸いです。(絶対に車輪の再開発してる気がするなーと思いながら作ってました)

7
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?