C#で長押しボタンを作る方法

産業系・制御系システムを構築する時は、

  • 間違えてワンクリックしただけで機械が動いてしまうと危ない
  • かといって毎回警告のポップアップを出すのは煩わしすぎる

など、「ボタンの長押し」をトリガーにしたいことがよくあります。

三菱製GOTタッチパネル等には最初から長押し機能が入っていますが、昨今はWindowsフォームアプリケーションからロボットを制御したいというケースもよくあります。
ということで、Windowsフォームアプリケーションで長押しボタンを作る方法を解説します。

仕様としては

  • ボタンを1秒間長押ししたら、機能を実行する
  • 1秒以上押し続けても、2回目は実行されない
  • 1秒未満でボタンを離せば、何も起こらない

とします。

サンプルコード

まず先に全体のサンプルコードを記載して、後から個別に解説します。

public partial class FrmMain : Form
{
    //ボタン長押し用タイマ
    private SystemThreading.Timer timer = null;
    //ボタン長押し中フラグ
    private bool flgPressing = false;

    public frmMain()
    {
        InitializeComponent();

        //ボタン長押し用タイマのコールバック登録
        timer = new SystemThreading.Timer(new TimerCallback(Callback_BtnSample), null, Timeout.Infinite, Timeout.Infinite);

        //コールバック関数
        public void Callback_BtnSample(Object obj)
        {
            if (this.InvokeRequired)
            {
                this.Invoke(new Action(BtnSample_LongClick));
            }
            else
            {
                BtnSample_LongClick();
            }
        }

        //長押し成立時に実行したい機能
        private void BtnSample_LongClick()
        {
            //任意の処理
        }

        //マウスが押され始めた時に起こるイベント
        private void BtnSample_MouseDown(object sender, MouseEventArgs e)
        {
            //一秒後に一度だけ発動
            timer.Change(1000, Timeout.Infinite);
            flgPressing = true;
        }
        //マウスが離された時に起こるイベント
        private void BtnSample_MouseUp(object sender, MouseEventArgs e)
        {
            //ボタンを離した時にタイマ無効化
            timer.Change(Timeout.Infinite, Timeout.Infinite);
            flgPressing = false;
        }
    }
}

次の項で、個別に解説していきます。

タイマとフラグの宣言

長押しを実現するために、Threading.Timerクラスを使用します。
また、ボタンが押されている状態を表すのにフラグも作っておきます。
「押している間ボタンの色を変える」などの制御がやりやすくなりますね。

//ボタン長押し用タイマ
private SystemThreading.Timer timer = null;
//ボタン長押し中フラグ
private bool flgPressing = false;

タイマのコールバック登録

タイマのコールバック関数を登録します。
ここでポイントとなるのが、第3引数dueTimeと第4引数PeriodをTimeout.Infiniteとしていること。
こうすることで、タイマは一旦、何も起動しない状態として設定されます。

//ボタン長押し用タイマのコールバック登録
timer = new SystemThreading.Timer(new TimerCallback(Callback_BtnSample),null,Timeout.Infinite,Timeout.Infinite);

↓Timer関数のパラメータ仕様は以下URLを参照

https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.timer.-ctor?view=net-6.0#system-threading-timer-ctor(system-threading-timercallback-system-object-system-int32-system-int32)

サブスレッドから扱うための「Invoke」

コールバック関数を見ると、わざわざもうワンクッション置いていることがわかります。
こんなことをしなくても正常に動きそうですが、実際に動かしてみると例外が発生して動かなくなります。
これは「異なるスレッドのオブジェクトを操作しようとしている」から。

Threading.Timerのイベント処理が動作するのはサブスレッド。
フォーム上のボタン等はメインスレッドで生成されています。

このようにスレッドを跨ぐ操作を行いたいときには、特別な手続きが必要です。
それがinvokeです。

  • InvokeRequiredを使い、現在サブスレッドから処理が動いているか(Invokeの必要があるかどうか)を判断する
  • 必要ならばInvoke()関数を使って、”実行したい機能”の関数ポインタを渡す

という流れになっています。

//コールバック関数
public void Callback_BtnSample(Object obj)
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action(BtnSample_LongClick));
    }
    else
    {
        BtnSample_LongClick();
    }
}

//長押し成立時に実行したい機能
private void BtnSample_LongClick()
{
    //任意の処理
}

MouseDown/MouseUpイベントの登録

最後にマウスのイベントを作成します。
MouseDown時にタイマのdueTimeを1000ミリ秒に設定することで、ボタンが押されてから1秒後にイベントが発火します。
MouseUp時にタイマのdueTimeをInfiniteに戻すことで、1秒未満でボタンを離せば何も起こらないようにできます。
periodはずっとInfiniteなので、押しっぱなしでも何度もイベントが発火することはありません。

※FormのDesigner.csでイベントハンドラの追加をするのを忘れずに!

//マウスが押され始めた時に起こるイベント
private void BtnSample_MouseDown(object sender, MouseEventArgs e)
{
    //一秒後に一度だけ発動
    timerBtnSample.Change(1000, Timeout.Infinite);
    PressingBtnSample = true;
}
       
//マウスが離された時に起こるイベント
private void BtnSample_MouseUp(object sender, MouseEventArgs e)
{
    //ボタンを離した時にタイマ無効化
    timerBtnSample.Change(Timeout.Infinite, Timeout.Infinite);
    PressingBtnSample = false;
}

以上で

  • ボタンを1秒間長押ししたら、機能を実行する
  • 1秒以上押し続けても、2回目は実行されない
  • 1秒未満でボタンを離せば、何も起こらない

という仕様の長押しボタンの完成です!