AccessVBAでクリック時に全選択を共通部品化:数字を消さずに上書き入力

AccessVBAでクリック時に全選択する処理のイメージ AccessVBA

AccessVBAでクリックしたら全選択、数字をそのまま上書きしたい。

AccessVBAで入力フォームを作っていると、利用者から次のように言われることがあります。

マウスでクリックして移動すると数字が上書きにならず、毎回0を消す手間がある

Accessでは、TabキーやEnterキーで移動したときはフィールド全体が選択されるので、そのまま入力すれば上書きになります。ところがマウスでクリックして移動したときは、クリックした位置にカーソルが置かれるだけです。「0」が入っている欄をクリックして「5」と打つと、「05」や「50」になってしまいます。

シチュエーション

数量・単価の入力画面
初期値の「0」が入っている欄に、毎回BackSpaceしてから入力している
テンキーとマウスを行き来しながら入力する

明細の修正
一覧のあちこちをクリックして値を直していく
金額を丸ごと入れ替えることがほとんど

マウス中心で操作する利用者
キーボードで移動する習慣がなく、クリックで欄を選ぶ

テキストボックスのクリック時イベントで SelStartSelLength を設定すれば全選択できます。ただ、欄が何十個もあると、1つずつイベントプロシージャを書くのは大変です。そこで、イベントプロパティに関数名を書くだけで使える共通部品を紹介します。

「クリックのたびに全選択」ではダメな理由

最初に思いつくのは、クリック時イベントで毎回全選択する方法です。

Private Sub Txt_Suryo_Click()
    Me.Txt_Suryo.SelStart = 0
    Me.Txt_Suryo.SelLength = Len(Me.Txt_Suryo.Text)
End Sub

この方法だと、クリックでカーソル位置を指定できなくなります。「12500」の「2」だけ直したくてクリックしても、全選択されてしまいます。部分的に直すには矢印キーで移動するしかなく、マウス中心の利用者にはかえって使いにくくなります。

そこで今回は、次の動きにします。

操作動き
別の欄からクリックで入ったとき全選択(そのまま上書き入力できる)
同じ欄をもう一度クリックしたとき通常どおりクリック位置にカーソル(部分修正できる)
TabキーやEnterキーで入ったときAccess標準の動き(全選択)

「欄に入った直後の1回だけ全選択する」という考え方です。

クリック時に全選択を行うプロシージャサンプル

標準モジュールに以下のコードを貼り付けます。

クリックで入ったかどうかをAPIで見る理由
フォーカス取得時のイベントからは、マウスで入ったのかキーボードで入ったのかが分かりません。すべての場合に予約すると、Tabキーで入った欄をクリックしてカーソル位置を指定しようとしたときにも全選択されてしまいます。そこで GetAsyncKeyState でマウスの左ボタンが押されているかを確認し、クリックで入ったときだけ予約しています。PtrSafe を付けているので、32ビット版・64ビット版のどちらのAccessでも動きます。

宣言はモジュールの先頭に
コード冒頭の Declare 文と Private m_blnSelPending As Boolean は、モジュールの先頭(最初のプロシージャより上)に置いてください。既存のモジュールの末尾にそのまま貼り付けると、「コンパイル エラー: End Sub、End Function または End Property 以降には、コメントのみが記述できます。」になります。

'--- マウスボタンの状態を調べるWindows API ---
Private Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer

'--- 全選択の予約フラグ(クリックで欄に入った直後だけTrue)---
Private m_blnSelPending As Boolean

'=========================================================
'  プロシージャ名:gf_SEL_ENTER
'  機能  :マウスのクリックで欄に入ったときだけ、
'            全選択を予約します。
'            テキストボックスの「フォーカス取得時」に設定します。
'
'  【設定例】
'    フォーカス取得時 = =gf_SEL_ENTER()
'=========================================================
Public Function gf_SEL_ENTER()
    '=== 左ボタンが押されている=クリックで入った ===
    m_blnSelPending = (GetAsyncKeyState(vbKeyLButton) And &H8000) <> 0
End Function

'=========================================================
'  プロシージャ名:gf_SEL_CLICK
'  機能  :予約されていれば、アクティブなコントロールの
'            内容を全選択します。
'            テキストボックスの「クリック時」に設定します。
'
'  【設定例】
'    クリック時 = =gf_SEL_CLICK()
'=========================================================
Public Function gf_SEL_CLICK()
    On Error Resume Next
    '=== 予約がなければ何もしない(2回目以降のクリック)===
    If Not m_blnSelPending Then Exit Function
    m_blnSelPending = False
    '=== 全選択 ===
    With Screen.ActiveControl
        .SelStart = 0
        .SelLength = Len(.Text)
    End With
End Function

'=========================================================
'  プロシージャ名:gf_SEL_CHANGE
'  機能  :入力が始まったら全選択の予約を解除します。
'            テキストボックスの「変更時」に設定します。
'
'  【設定例】
'    変更時 = =gf_SEL_CHANGE()
'=========================================================
Public Function gf_SEL_CHANGE()
    m_blnSelPending = False
End Function

仕組みは次のとおりです。

  1. 欄に入ると フォーカス取得時gf_SEL_ENTER が動く。マウスの左ボタンが押されていれば(=クリックで入った)「全選択の予約」を立てる
  2. 続く クリック時gf_SEL_CLICK が動き、予約があれば全選択して予約を解除する
  3. 同じ欄をもう一度クリックしても、予約は解除済みなので何もしない(通常のカーソル移動)
  4. TabキーやEnterキーで入ったときは予約しないので、その後クリックしても通常のカーソル移動になる
  5. 予約が残ったまま文字を打ち始めた場合は、変更時gf_SEL_CHANGE で予約を解除する

フォーカス取得時で全選択しない理由
マウスでクリックして欄に入ったとき、イベントは「フォーカス取得時 → フォーカス取得後 → マウスボタンクリック時 → マウスボタン解放時 → クリック時」の順に発生します。フォーカス取得時に全選択しても、その後のマウス操作でカーソル位置が上書きされてしまいます。そのため、全選択はクリック時で行い、フォーカス取得時は予約だけにしています。

対象のコントロールは Screen.ActiveControl で取得しているので、どの欄に設定しても同じ関数で動きます。

実行方法

フォームをデザインビューで開き、対象のテキストボックスのプロパティシート「イベント」タブに以下を入力します。

プロパティ設定値
フォーカス取得時=gf_SEL_ENTER()
クリック時=gf_SEL_CLICK()
変更時=gf_SEL_CHANGE()
数量のテキストボックスのプロパティシート(イベントタブ)に3つの関数を設定した画面

複数のテキストボックスをまとめて選択してから入力すれば、一度に設定できます。

イベントプロパティに =関数名() と書く方法なら、フォームのモジュールには1行もVBAを書く必要がありません。欄を追加したときも、プロパティをコピーするだけで済みます。

※イベントプロパティから呼び出すため、Sub ではなく Function にしています。また、標準モジュールに Public で置く必要があります。

既にイベントプロシージャがある欄の場合

IMEの切り替えなどで、既に「フォーカス取得時」に [イベント プロシージャ] が設定されている欄もあります。その場合は、プロパティはそのままにして、プロシージャの最後に1行追加します。

Private Sub Txt_Hinmei_Enter()
    '=== 既存の処理(IMEをオンにする等)===
    Me.Txt_Hinmei.IMEMode = 1
    '=== 全選択を予約 ===
    Call gf_SEL_ENTER
End Sub

クリック時・変更時が空であれば、そちらはプロパティに =gf_SEL_CLICK()=gf_SEL_CHANGE() を設定すれば共存できます。

入力イメージ

数量に「0」、単価に「12500」が入っている受注入力フォームで、変更前(Access標準)と変更後を比べてみます。どちらも、数量と単価をマウスでクリックしてから「5」「9800」と入力しています。

変更前(Access標準)
クリックした位置にカーソルが置かれるだけなので、「0」の後ろに「5」が入って「05」に、単価は「125009800」になってしまいます。

変更前:数量をクリックして5と入力すると05になり、単価は125009800になる

変更後(gf_SEL_ENTER / gf_SEL_CLICK / gf_SEL_CHANGE を設定)
クリックした時点で中身が全選択されるので、そのまま「5」「9800」に上書きされます。

変更後:数量をクリックすると0が全選択され、5と入力すると5に上書きされる

「12500」の一部だけ直したいときは、もう一度クリックすればクリックした位置にカーソルが移動するので、通常どおり部分修正ができます。

フォーム内のテキストボックスにまとめて設定したい。

欄の数が多いフォームでは、プロパティを1つずつ設定するのも手間です。デザインビューでフォームを開き、テキストボックスにまとめて設定するプロシージャも書いてみました。

'=========================================================
'  プロシージャ名:gs_SET_SEL_EVENT
'  機能  :指定フォームの入力可能なテキストボックスに
'            全選択用のイベントプロパティを一括設定します。
'
'  【引数】
'    strFormName :対象フォーム名
'
'  【処理概要】
'    ・フォームをデザインビューで開く
'    ・入力可能なテキストボックスだけを対象にする
'      (使用不可・編集ロック・演算コントロールは対象外)
'    ・既にイベントが設定されているプロパティは上書きしない
'    ・保存して閉じ、設定件数をイミディエイトウィンドウに出力
'
'  【使用例】
'    Call gs_SET_SEL_EVENT("F_受注入力")
'=========================================================
Public Sub gs_SET_SEL_EVENT(strFormName As String)
    On Error GoTo err_SET_SEL_EVENT
    Dim l_frm As Form             ' 対象フォーム
    Dim l_ctl As Control          ' コントロール
    Dim l_cnt As Long             ' 設定件数
    Dim l_skip As Long            ' 既存イベントありで一部スキップした件数
    '=== デザインビューで開く ===
    DoCmd.OpenForm strFormName, acDesign, , , , acHidden
    Set l_frm = Forms(strFormName)
    For Each l_ctl In l_frm.Controls
        If l_ctl.ControlType = acTextBox Then
            '=== 入力できない欄は対象外 ===
            If l_ctl.Enabled And Not l_ctl.Locked _
               And Left(l_ctl.ControlSource, 1) <> "=" Then
                '=== 空のプロパティだけ設定(既存イベントは上書きしない)===
                If Len(l_ctl.OnEnter) = 0 Then l_ctl.OnEnter = "=gf_SEL_ENTER()"
                If l_ctl.OnEnter <> "=gf_SEL_ENTER()" Then l_skip = l_skip + 1
                If Len(l_ctl.OnClick) = 0 Then l_ctl.OnClick = "=gf_SEL_CLICK()"
                If Len(l_ctl.OnChange) = 0 Then l_ctl.OnChange = "=gf_SEL_CHANGE()"
                l_cnt = l_cnt + 1
                Debug.Print strFormName & "." & l_ctl.Name & _
                            IIf(l_ctl.OnEnter = "=gf_SEL_ENTER()", "", " ※フォーカス取得時は既存のため要手動追加")
            End If
        End If
    Next l_ctl
    '=== 保存して閉じる ===
    DoCmd.Close acForm, strFormName, acSaveYes
    Debug.Print "設定件数:" & l_cnt & "件(うちフォーカス取得時が既存:" & l_skip & "件)"
    Exit Sub
err_SET_SEL_EVENT:
    MsgBox "【gs_SET_SEL_EVENT】設定中にエラー発生" & vbCrLf & _
           "番号:" & Err.Number & vbCrLf & _
           "内容:" & Err.Description, vbCritical
    '=== デザインビューで開いたままにしない(変更は保存しない)===
    On Error Resume Next
    DoCmd.Close acForm, strFormName, acSaveNo
End Sub

イミディエイトウィンドウで次のように実行します。

Call gs_SET_SEL_EVENT("F_受注入力")

「※フォーカス取得時は既存のため要手動追加」と出た欄は、前の章の方法でプロシージャに Call gf_SEL_ENTER を追加してください。

サブフォームは別のフォームなので、サブフォーム側のフォーム名でも実行します。

一括設定するときの注意点

実際に業務システムの70以上の欄に一括設定したとき、次の点でつまずきました。

デザイン時は編集ロックでも、実行時に解除される欄がある
「条件によって入力可にする」欄は、デザイン時に Locked = True になっていることがあります。上のプロシージャでは対象外になるため、設定漏れになります。コード内の .Locked = False を検索して、該当する欄がないか確認してください。

IIfは両方の式が評価される
設定結果を確認するコードで、コントロールの種類によって取得するプロパティを変えようとして、

Debug.Print IIf(l_ctl.ControlType = acComboBox, l_ctl.LimitToList, "-")

のように書くと、テキストボックスでもコンボボックス専用の LimitToList が評価されてエラーになります。On Error Resume Next と併用していると、その行がエラーも出さずに飛ばされて集計から抜け落ちるので、件数が合わない原因になります。種類で分岐するときは If 文で書きます。

コンボボックスについて
コンボボックスの「クリック時」イベントは、テキストボックスと違いリストから項目を選んだときに発生します。本記事ではテキストボックスを対象にしています。

最後に

以上がAccessVBAで、クリック時に全選択する処理を共通部品化する方法です。

「0を消してから入力する」は1回あたり一瞬の手間です。それでも、明細を何十行も入力する画面では積み重なり、利用者の不満になりやすいポイントです。

イベントプロパティに =関数名() を書く方法なら、フォーム側にVBAを書かずに済むので、欄が多い画面でも短時間で対応できます。「入った直後の1回だけ全選択」にしておけば、上書き入力も部分修正もどちらもマウスで行えます。

コメント

タイトルとURLをコピーしました