WinFormsで別クラスからLabelの文字を変えるには?設計方法を比べてみよう

広告

WinFormsアプリを作っていると、Form1以外のクラスから画面上のLabelやTextBoxを変更したくなることがあります。

たとえば、Form1に label1 が置かれていて、別のクラスからその文字を変更したい場合です。

このとき、実は書き方はいくつもあります。

今回は、

  • Labelコントロールをそのまま渡す方法
  • 戻り値を使う方法
  • イベントを使う方法
  • コールバックを使う方法
  • Form1そのものを渡す方法
  • インターフェースを使う方法

を順番に見ていきます。


1. Labelコントロールをそのまま渡す

Labelコントロールをそのまま渡す方法のイラスト

まずは、一番わかりやすい方法です。

class MessageMaker
{
    public void ChangeLabel(Label label)
    {
        label.Text = "こんにちは";
    }
}

Form1側では次のように呼び出します。

private void button1_Click(object sender, EventArgs e)
{
    MessageMaker maker = new MessageMaker();
    maker.ChangeLabel(label1);
}

この書き方では、label1 そのものを引数として渡しています。

つまり、

Form1
  ↓
label1を渡す
  ↓
MessageMaker
  ↓
label.Textを書き換える

という流れです。

この方法は間違いなの?

いいえ、間違いではありません。

LabelもC#ではオブジェクトなので、メソッドの引数として渡すことができます。

特に学習段階では、

コントロールもオブジェクトとして扱える

ことを理解する良い例になります。

また、UI専用のクラスであれば、実務でもこのような書き方をすることがあります。

たとえば、

class LabelHelper
{
    public void ShowError(Label label, string message)
    {
        label.Text = message;
        label.ForeColor = Color.Red;
        label.Visible = true;
    }
}

のようなクラスです。

このクラスは「Labelを操作すること」が目的なので、Labelを受け取ることに違和感はありません。


2. Labelを渡すデメリット

問題になるのは、本来UIと関係ないクラスまでLabelを知ってしまう場合です。

たとえば、計算を担当するクラスが次のようになっていたとします。

class Calculator
{
    public void Add(Label label, int x, int y)
    {
        int answer = x + y;
        label.Text = answer.ToString();
    }
}

一見すると便利ですが、Calculator は「計算するクラス」なのに、Labelまで知っています。

すると、

計算処理と画面処理が強く結びついてしまう

という問題が出てきます。

このような状態を「結合度が高い」と表現することがあります。


3. 戻り値を使う方法

戻り値を使う方法のイラスト

計算クラスであれば、結果だけを返す方が自然です。

class Calculator
{
    public int Add(int x, int y)
    {
        return x + y;
    }
}

Form1では、

private void button1_Click(object sender, EventArgs e)
{
    Calculator calc = new Calculator();

    int answer = calc.Add(10, 20);

    label1.Text = answer.ToString();
}

とします。

この場合、

Form1
  ↓
10と20を渡す

Calculator
  ↓
30を返す

Form1
  ↓
label1に30を表示する

という役割分担になります。

ポイント

Calculatorは、

計算することだけを担当

Form1は、

画面に表示することを担当

しています。

このように、それぞれのクラスが自分の役割に集中する設計は、プログラムが大きくなるほど重要になります。


4. Labelを渡す方法と戻り値方式を比べる

違いを整理してみましょう。

Labelを渡す方法

maker.ChangeLabel(label1);

別クラスが直接Labelを書き換えます。

戻り値方式

string message = maker.GetMessage();
label1.Text = message;

別クラスは結果だけ返し、画面の変更はForm1が行います。

どちらが良いの?

どちらも使えます。

重要なのは、

そのクラスが何を担当するクラスなのか

という点です。

UIを操作するクラスならLabelを受け取るのも自然です。

計算やデータ処理をするクラスなら、Labelを受け取らない方が設計しやすくなります。


5. イベントを使う方法

イベントを使う方法のイラスト

次は少し発展です。

別クラスの処理結果をForm1へ通知したい場合、イベントを使う方法があります。

class Worker
{
    public event Action<string> MessageChanged;

    public void Run()
    {
        MessageChanged?.Invoke("処理を開始しました");
    }
}

Form1側では、

private void button1_Click(object sender, EventArgs e)
{
    Worker worker = new Worker();

    worker.MessageChanged += message =>
    {
        label1.Text = message;
    };

    worker.Run();
}

と書きます。

この場合、WorkerはLabelの存在を知りません。

Workerが知っているのは、

文字列を通知する

ということだけです。

流れは次のようになります。

Worker
  ↓
「処理を開始しました」と通知
  ↓
イベント
  ↓
Form1
  ↓
label1.Textを変更

イベント方式のメリット

イベントを使うと、

別クラスがForm1を直接知らなくてもよい

というメリットがあります。

処理の進捗表示や完了通知などでもよく使われます。

補足すると、ここで出てきた message => { label1.Text = message; }; という書き方はラムダ式と呼ばれるものです。「messageを受け取ったら、{ }の中の処理を実行する」という小さな無名の関数を、その場で作って渡していると考えるとイメージしやすくなります。イベントやコールバックでは、このように処理そのものを短く書いて相手に渡す場面がよく出てきます。


6. コールバックを使う方法

コールバックを使う方法のイラスト

イベントより簡単に「後でForm1側の処理を呼び出したい」場合は、Action を使う方法もあります。

class Worker
{
    public void Run(Action<string> showMessage)
    {
        showMessage("処理中です");
    }
}

Form1では、

private void button1_Click(object sender, EventArgs e)
{
    Worker worker = new Worker();

    worker.Run(message =>
    {
        label1.Text = message;
    });
}

と書けます。

このように、

呼び出してほしい処理そのものを渡す

方法をコールバックと呼ぶことがあります。


7. Form1そのものを渡す方法

Form1そのものを渡す方法のイラスト

Form1自体を別クラスへ渡す方法もあります。

class Worker
{
    private Form1 form;

    public Worker(Form1 form)
    {
        this.form = form;
    }
}

Form1では、

Worker worker = new Worker(this);

とします。

この方法なら、WorkerからForm1のメンバーへアクセスできます。

ただし、WorkerがForm1という特定の画面に強く依存してしまいます。

そのため、規模が大きくなるほど変更しにくい設計になります。

小規模なツールでは使われることもありますが、まずは戻り値やイベント方式を考える方がよいでしょう。


8. インターフェースを使う方法

インターフェースを使う方法のイラスト

さらに発展すると、インターフェースを使う方法があります。

まず、

interface IMessageView
{
    void ShowMessage(string message);
}

というインターフェースを作ります。

Form1で実装します。

public partial class Form1 : Form, IMessageView
{
    public void ShowMessage(string message)
    {
        label1.Text = message;
    }
}

WorkerはForm1そのものではなく、IMessageView を受け取ります。

class Worker
{
    private IMessageView view;

    public Worker(IMessageView view)
    {
        this.view = view;
    }

    public void Run()
    {
        view.ShowMessage("処理完了");
    }
}

Form1では、

Worker worker = new Worker(this);
worker.Run();

と呼び出せます。

この書き方のポイントは、

WorkerはForm1を知らない

という点です。

知っているのは、

IMessageView

という約束だけです。

これにより、クラス同士の結びつきを弱くできます。

余談ですが、この「Form1そのものではなく、約束(インターフェース)に依存する」という考え方は、MVP(Model-View-Presenter)やDI(依存性注入)と呼ばれる設計手法の入り口にもなっています。ここで体験した「具体的なクラスではなく、約束ごとに依存する」という発想は、この先設計パターンを学ぶときにもそのまま役に立ちます。


9. 実務ではどう使い分ける?

目安としては、次のように考えるとよいでしょう。

方法向いている場面
Labelを渡すUI専用のヘルパー
戻り値計算・文字列生成・データ処理
イベント進捗通知・完了通知
コールバック小規模で簡単な通知
Form1を渡す小規模アプリ
インターフェース規模が大きいアプリ

10. 「オブジェクトを渡すこと」が悪いわけではない

ここは非常に重要です。

今回問題にしているのは、

オブジェクトを引数として渡すこと

ではありません。

C#では、オブジェクトを引数として渡すことは普通にあります。

たとえば、

Student student
Customer customer
Order order
List<string> items

などをメソッドへ渡すことはよくあります。

問題になるのは、

そのクラスが本来知る必要のないオブジェクトまで渡してしまうこと

です。

たとえば、

Label
TextBox
Button
Form

などのUI部品を、計算クラスやデータ処理クラスへ渡し始めると、画面と処理が強く結びついてしまいます。


11. 学習するときのおすすめの順番

初心者のうちは、次の順番で学ぶと理解しやすいです。

Labelを渡す
   ↓
オブジェクトも引数として渡せると理解する

戻り値を使う
   ↓
クラスごとの役割を考える

イベントを使う
   ↓
別クラスからForm1へ通知する

インターフェースを使う
   ↓
クラス同士の結びつきを弱くする

最初にLabelを引数として渡していたからといって、それが間違いだったわけではありません。

むしろ、

まずは動かして理解する
その後で設計を考える

という学習の流れは自然です。


まとめ

WinFormsで別クラスから画面を変更する方法はいくつもあります。

一番大切なのは、

「このクラスは何を担当するクラスなのか?」

を考えることです。

UIを扱うクラスなら、Labelを渡しても問題ありません。

計算やデータ処理を担当するクラスなら、結果を戻り値で返し、Form1側でLabelへ表示する方が自然です。

さらにプログラムが大きくなってきたら、イベントやインターフェースを使うことで、より変更しやすい設計にできます。

最初から難しい設計を覚える必要はありません。

まずは、

動くコードを書く
↓
役割を分ける
↓
結びつきを弱くする

という順番で考えていきましょう。

訪問数 11 回, 今日の訪問数 11回

広告

C#

Posted by hidepon