Visual Basic 6.0 初級講座
VB6対応

 

Visual Basic 中学校 > VB6 初級講座 >

第30回 同じコードを2度書くな

 

初級講座最終回の今回は複数のコントロールのイベントを一ヶ所で処理する方法を説明します。同じようなプログラムをコントロールの数だけコピー&貼り付けするようなプログラムは書く必要がありません。洗練されたエレガントなプログラムを目指します。

この回の要約

・クラスでWithEvents付きで宣言した変数のイベントはクラスでもトラップできる。

 

1.優秀なプログラマ

 

逆説的で気障な言い方をすると、もっとも良いプログラマとはプログラムを書かないプログラマです。プログラムを書かないのですから絶対にバグが発生することがありません。

もう少し現実的な言い方に言い換えると、良いプログラマとは少ししかプログラムを書かないプログラマです。プログラムの量が少ないのでバグも少ないですし開発のスピードも速いです。

しかし、どうすればプログラムを少ししか書かないで済むでしょうか?あなたはあきらめていませんか?プログラムをガリガリ書くのが当たり前だと思っていませんか?または、日々の忙しさに追われて今まで繰り返してきたことを何も考えずにまた繰り返していませんか?

プログラムの量を減らす方法はたくさんありますが、私が声を大にしていいたいのは「同じコードを2度書くな」ということです。

たとえば、10バイトまでしか入力できないように制限したいテキストボックスがあったとしましょう。そして、入力された内容が10バイト以内であれば、[Enter]キーが押されたときに次のテキストボックスにフォーカスを移しますが、10バイトを超えている場合はエラーメッセージを表示します。

このプログラムの例をして次のようなコードが考えられます。

VB6対応

Private Sub Text1_KeyPress(KeyAscii As Integer)

    Dim ByteLength As Integer

    If KeyAscii = vbKeyReturn Then

        KeyAscii = 0
        ByteLength = LenB(StrConv(Text1.Text, vbFromUnicode))

        If ByteLength <= 10 Then
            '次のコントロールにフォーカスをセット(SetFocusは使わない)
            Text1.Enabled = False
            Text1.Enabled = True
        Else
            'エラーメッセージ表示
            MsgBox "10バイト以内で入力してください。", vbExclamation, "エラー"
        End If

    End If

End Sub

経験豊富なプログラマなら入力された文字列のバイト数を取得する部分は関数化して独立させるでしょう。また、エラーメッセージを表示する部分も別の仕組みを利用するでしょう。しかし、今回はわかりやすさを重視してその部分はこのままにしておきます。

さて、このコードのどこに問題があるかわかりますか?

実はこのコード自体には何の問題もありません。よくできています。

ところが、別のテキストボックスでは今度は8バイトまでしか入力できないようにしたいとしたらどうでしょうか?そして、このテキストボックスでもやはり[Enter]キーを押して8バイト以内なら次のコントロールにフォーカスを移し、そうでないならばエラーメッセージを表示するとしたら?

このような要求は実際のプログラムではごくありふれたことです。むしろテキストボックスごとに異なった仕様になっている方が珍しいといえるでしょう。

さて、あなたはやむを得ずまた似たようなプログラムを書きますか?既に完成しているコードをコピー貼り付けして「10バイト」の部分を「8バイト」に変えるだけですからたいした手間はありませんよね。私も、テキストボックスが2つしかなくて、将来も3つになったりしないということがはっきりしていればそうするでしょう。

しかし、テキストボックスがもっとたくさんあったらどうでしょうか?テキストボックスが10個も20個もあって、しかも将来もその数は増えるかもしれないというときにあなたは「同じコードを何度も書く」のでしょうか?

ここが優秀なプログラマとそうでないプログラマの分かれ道です。もし、プロのプログラマが同じようなコードをコピー貼り付けして10個も20個も書いたとするとそのプログラマはプロ失格です。

これから、この場合において同じコードを2度以上書かないで済む方法を紹介します。

 

2.同じコードを2度書いてはいけない理由

 

その前に、同じコードを何度も書いてはいけない理由を説明しましょう。同じコードを何度も書いた場合、以下のようなデメリットが発生します。

・時間がかかる

・大量の「コピー&貼り付け」は人間なのでミスが発生する可能性がある。

・この仕様が変更になった場合、修正個所が多すぎる。

この中でも最後に挙げた「修正個所が多すぎる」というのは決して見過ごすことはできません。20個所にコピーしたコードを修正するには、1箇所のコードを修正する場合の20倍の時間がかかるのです。つまり、本来1日で終わるはずの作業に20日もかかるということになります。

 

3.コードを一箇所にまとめる方法

 

今回のケースでは同じコードを大量にかかないで一箇所にまとめる代表的な方法は次の通りです。

TextBoxを自作する
イベントを一括トラップする
共通の関数を作って使用する

はじめの「TextBoxを自作する」というのはこのような機能を持ったテキストボックスのようなActiveXコントロールを作成するということです。ActiveXコントロールさえ作成してしまえば、通常のテキストボックスのように簡単に利用できますしプロパティウィンドウを使って入力可能なバイト数なども設定することができます。うまく作ればこの部分に関してはプログラムレスの開発が可能です。ただし、このActiveXコントロールをしっかり設計・プログラムしないと他の場所でいろいろな障害が発生するので注意が必要です。実際に私はこのような失敗例をたくさん目撃してきました。

特ににわか勉強で作成されたActiveXコントロールほど恐ろしいものはありません。なにしろ、自作のActiveXコントロールなのですからインターネットや書籍で探しても情報がないわけです。だから、あることを実現させようと思ったときにActiveXコントロール側でその仕組みをあらかじめ用意しておいてくれないとどうしようもなくなってしまいます。今回のケースでは、「他の機能はすべてテキストボックスと同じで、ただバイト数のチェックと、フォーカスの移動、エラーメッセージの表示機能だけが追加されている」というActiveXコントロールを作ればよいでしょう。

※今回の例を実際的に考えると、エラーメッセージをデータベースなど外部の媒体から取得する可能性が考えられます。この場合ActiveX側でこの外部の媒体をどのように呼び出すかは慎重に決定する必要があります。たとえばActiveXが自らデータベースへ接続し、エラーメッセージを取得するようにすべきでないことは明白です。ではどうすればよいでしょうか?こういったことに対する設計ミスがActiveXを失敗へと導きます。

次の「イベントを一括トラップする」というのは各テキストボックスのKeyPressイベントを各テキストボックスごとにトラップするのではなく一箇所でトラップすることを指しています。クラスとWithEventsキーワードを使用すればこのようなことも可能なのです。

最後の「共通の関数を作って使用する」とは、各KeyPressイベントでは関数を呼ぶだけで、すべての処理はこの共通の関数で行うことを指しています。入力可能なバイト数と、対象のテキストボックスを引数に取るようにすればこのようなことも可能です。しかし、各KeyPressイベントに適切に関数を呼ぶようにプログラムしなければいけないので簡便さはそこそこです。

 

これらの方法の中で、プログラマにちゃんとした本物のスキルがあるのであれば「TextBoxを自作する」つまりActiveXコントロールを作成するというのが たいていの場合は最も良い方法です。しかし、VBの評価版やStandardエディションではActiveXコントロールを自作する機能がありません。それに「イベントを一括トラップする」つまりクラスをWithEventsキーワードを使う方法のほうが柔軟性があって私の好みですのでここではこの方法を説明することにします。

最後の「共通の関数を作って使用する」方法は、説明するまでもなくどういうことなのか理解していただけるでしょう。

 

4.イベントの一括トラップ

 

まずは、Text1KeyPressイベントとText2KeyPressイベントを同じプロシージャで処理できるようにしてみましょう。

プロジェクトに新しくクラスを追加してください。クラスの名前は今回は「MultiEvent」とします。

クラスMultiEventに次の通りにコードを追加してください。

VB6対応

Public WithEvents SlaveTextBox As TextBox
Private Sub SlaveTextBox_KeyPress(KeyAscii As Integer)

    MsgBox "私は" & SlaveTextBox.Name & "です。"

End Sub

このコードでは、対象のテキストボックスのKeyPressイベントが発生するとメッセージを表示します。イベントを一括トラップするための最もシンプルなコードです。宣言した変数のイベントをトラップするために前回も登場したWithEventsキーワードが必要になります。

また変数名はSlaveTextBoxとしましたが、当然自由に設定して構いません。ただし、この変数名とKeyPressイベントをトラップするプロシージャの名前は一致していなければなりませんから、変数名を変更する場合は忘れずにプロシージャ名も変更してください。

次に、どのテキストボックスが対象となるか指定するコードを書きます。これはForm1側に記述します。Form1にはTextBoxを3つ配置してください。そして次のように記述します。

VB6対応

Dim Multi(1) As New MultiEvent
Private Sub Form_Load()

    Set Multi(0).SlaveTextBox = Text1
    Set Multi(1).SlaveTextBox = Text2

End Sub

このコードではText1Text2MultiEvent側でイベントがトラップされます。Text3については何も指定していないので通常通りです。

早速実行してみてText1Text2に文字を入力してみてメッセージが表示されるのを確認してみてください。また、Text3をどんなに変更してもメッセージが表示されないとことも確認してみると良いでしょう。

さて、この仕組みを使えばいろいろなことができそうだと思いませんか?

それでは、最初に出てきた例を組み込んできます。つまり、Text1は10バイトまでしか入力できず、[Enter]キーを押したときに入力内容が10バイト以内なら次のコントロールにフォーカスを移します。そうでないならばエラーメッセージを表示します。Text2は同様のことを8バイトで行います。

まず、10バイトなのか8バイトなのかといった区別が必要ですからクラス側で既定のバイト数を記録しておく変数を作ります。また、TextBoxMultiEventを結びつけるときにこのバイト数を同時に指定できるように専用の関数Bindを作成します。

以上のことを考慮するとクラスMultiEventのプログラムは次のようになります。

VB6対応

Public WithEvents SlaveTextBox As TextBox
Public MaxByte As Integer
Public Sub Bind(SlaveTextBox As TextBox, MaxByte As Integer)

    Set Me.SlaveTextBox = SlaveTextBox
    Me.MaxByte = MaxByte

End Sub

Private Sub SlaveTextBox_KeyPress(KeyAscii As Integer)

    Dim ByteLength As Integer

    If KeyAscii = vbKeyReturn Then
       
KeyAscii = 0
        ByteLength = LenB(StrConv(SlaveTextBox.Text, vbFromUnicode))
        If ByteLength <= MaxByte Then
           
'次のコントロールにフォーカスをセット(SetFocusは使わない)
            SlaveTextBox.Enabled = False
           
SlaveTextBox.Enabled = True
        Else
           
'エラーメッセージ表示
            MsgBox MaxByte & "バイト以内で入力してください。", vbExclamation, "エラー"
        End If
    End If

End Sub

フォーム側のプログラムもこれに対応して次のように変更してください。

VB6対応

Dim Multi(1) As New MultiEvent
Private Sub Form_Load()

    Multi(0).Bind Text1, 10
    Multi(1).Bind Text2, 8

End Sub

すばらしいことにこれだけで完成です。

これでText1Text2には[Enter]キーを押したときに自動的にプログラムが実行されます。しかもText3でも同じことがやりたいと思ったらフォーム側のプログラムを次のように変更するだけです。

VB6対応

Dim Multi(2) As New MultiEvent
Private Sub Form_Load()

    Multi(0).Bind Text1, 10
    Multi(1).Bind Text2, 8
    Multi(2).Bind Text3, 10

End Sub

この方法によって実際のプログラムの作業量がどれだけ減少するか考えてみてください。テキストボックスがいくつあってもメッセージを変更するには一ヶ所直せばよいだけです。バイト数ではなく文字数で判断するように修正するのも一ヶ所直せばよいだけです。

またテキストボックスを追加するには2ヶ所変更するだけです。フォーム側のプログラムもとても見やすくなっています。

 

5.異なる種類のコントロール

 

今度はこのフォームにComboBoxを追加してください。このComboBoxもテキストボックスと同じように[Enter]キーが押されたときに同じプロシージャでイベントを処理するようにしてみましょう。

まず、クラス側の宣言部でPublic WithEvents SlaveTextBox As TextBoxと書いてあるのが気になるでしょう。ComboBoxも対象にしたいのでとりあえず、As Controlとか、As Object, As Variantなど、TextBoxでもComboBoxでもセットできる型を指定したいと思うでしょう。

ところがこれはできないのです。WithEventsキーワードを使用している宣言では型をはっきりとしていしなければならないのです。

この問題が明らかになったときにTextBoxComboBoxで同じプロシージャでイベントを処理することをあきらめてしまう人もいるかもしれませんが抜け道はあります。結論からお見せしましょう。クラス側のプログラムは次のようになります。

VB6対応

Public SlaveControl As Control
Public WithEvents SlaveTextBox As TextBox
Public WithEvents SlaveComboBox As ComboBox
Public MaxByte As Integer
Public Sub Bind(SlaveControl As Control, MaxByte As Integer)

    If TypeOf SlaveControl Is TextBox Then
        Set Me.SlaveTextBox = SlaveControl
    ElseIf TypeOf SlaveControl Is ComboBox Then
        Set Me.SlaveComboBox = SlaveControl
    End If

    Set Me.SlaveControl = SlaveControl
    Me.MaxByte = MaxByte

End Sub

Private Sub SlaveControl_KeyPress(KeyAscii As Integer)

    Dim ByteLength As Integer

    If KeyAscii = vbKeyReturn Then
        KeyAscii = 0
        ByteLength = LenB(StrConv(SlaveControl.Text, vbFromUnicode))
        If ByteLength <= MaxByte Then
            '次のコントロールにフォーカスをセット(SetFocusは使わない)
            SlaveControl.Enabled = False
            SlaveControl.Enabled = True
        Else
            'エラーメッセージ表示
            MsgBox MaxByte & "バイト以内で入力してください。", vbExclamation, "エラー"
        End If
    End If

End Sub

Private Sub SlaveComboBox_KeyPress(KeyAscii As Integer)

    Call SlaveControl_KeyPress(KeyAscii)

End Sub

Private Sub SlaveTextBox_KeyPress(KeyAscii As Integer)

    Call SlaveControl_KeyPress(KeyAscii)

End Sub

結構長くなってしまいました。要するにTextBoxComboBoxは別々にWithEventsを宣言します。イベントプロシージャも別々です。ただし、そうするとクラスの中でTextBoxComboBoxのどちらが指定されているかいちいち調べなければならなくなりますから、対象がTextBoxであろうがComboBoxであろうが変数SlaveControlに対象をセットしておくようにします。これでSlaveControlを見れば対象のコントロールにすぐにアクセスできるようになるわけです。

また、KeyPressイベントもSlaveControlについて記述しておきます。ただし、SlaveControlControl型なのでWithEventsキーワードを使用できません。つまり自動的にイベントをトラップすることができません。そこでSlaveTextBoxSlaveComboBoxKeyPressイベントからSlaveControl_KeyPressプロシージャを自分で呼び出すコートを書く必要があります。

このようにクラス側の構造はやや複雑な様相を呈しますが、フォーム側ではこの違いを意識しなくて済むようにBindメソッドでこれらの違いを吸収します。このBindメソッドによりフォーム側のプログラムは先ほどと全く同じで動作します。

なお、プロシージャ名の「SlaveControl_KeyPress」はここでイベントを処理するという意味でイベントプロシージャのような名前をつけましたが、実際にはこのプロシージャは単なるプロシージャであってここで直接イベントをトラップするわけではないのでまったく好きな名前をつけても構いません。

念のためにフォーム側のプログラムも示しておきます。ComboBoxを1つ追加して次のようにコードを変更します。この例ではComboBoxには4バイトまでしか入力できません。

VB6対応

Dim Multi(3) As New MultiEvent
Private Sub Form_Load()

    Multi(0).Bind Text1, 10
    Multi(1).Bind Text2, 8
    Multi(2).Bind Text3, 10
    Multi(3).Bind Combo1, 4

End Sub

 

6.補足と課題

 

重要な説明を後回しにしてしまいました。クラスを使ってKeyPressイベントをトラップしている場合でも通常通りフォーム側でイベントをトラップすることもできます。この場合1回のKeyPressでフォーム側とクラス側の2つのイベントプロシージャが実行されることになります。順番はフォームのイベント→クラスのイベントとなります。

また、クラスを複数作成することもできますし、同じクラス内でTextBox型の変数をWithEvents付きで複数宣言することもできますから1回のKeyPressで実行できるイベントプロシージャの数は1つや2つではなく無数ということになります。

もちろんこのようなことをするとプログラムが複雑になるのでお勧めはできません。フォームで1つ、クラスで1つというのがわかりやすさをたもてる限界でしょう。とはいえいざというときにはこのような複数トラップが可能であるという知識が役に立つこともあるでしょう。

そして課題です。今回はシンプルな例で紹介しましたのでプログラムはシンプルになりました。しかし欲張ってあれもこれもクラスで処理するということになるとプログラムはたちまち複雑になりメンテ不能に陥ります。なにしろ一ヶ所直しただけでたくさんのイベントの動作が変更されてしまうのですから大変です。

そこで、クラスを使って欲張った機能を持たせるときは次の2点に注意してください。

1.事前にしっかりと完成図をイメージする(→つまり、いきあたりばったりで作成しない。)
2.クラス側では外部のPublicな変数にアクセスしない。

この2点は私の経験から導かれたものです。

今回の例に限ると他にも課題はあります。今回の例では[Enter]キーを押したときの動作は完璧ですが、[Tab]キーを押されたり、マウスを使ってフォーカスを移動する場合はイベントが発生しないので何のチェックも働きません。

トラップするイベントをKeyPressイベントではなくValidateイベントに変更する方法もありますが 動作が変わってしまいます。私が良く使う方法はクラス側に最終チェック用のメソッドを1つ用意して、フォーム側で[OK]ボタンまたはそれに類似する機能が呼び出されたときにこのチェックメソッドを呼び出してすべてのテキストボックスの状態を一度にチェックする方法です。

たとえば、次のようなものを想定しています。

VB6対応

Private Sub cmdOK_Click()

    Dim i As Integer

    For i = 0 To UBound(Multi())
        If Multi(i).Check = False Then
            'ここに入力に間違いがある場合の処理を書く
            Exit Sub
        End If
    Next
i

    Call SaveData

End Sub

もちろん、この場合はクラス側でCheckメソッドとKeyPressイベントに同じコードを書かないで済むようなプログラミングをすることが望ましいです。

 

7.初級講座の最後に

 

以上で6年の長い間にわたって連載してきた初級講座は終了です。もちろん、記事の修正などは今後も発生するかもしれませんが初級講座はここに一応の完結となります。Visual Basic 中学校の一番最初の記事は初級講座の第1回でした。それは1999年の10月ごろのことです。

長い連載を読んでいただいた方は本当にありがとうございます。

この連載当初の私のVBの知識や理解度は現在の私の知識や理解度とくらべ、とても低レベルなものでした。後になって記事を読み返してみると不適切な説明や間違った説明を発見することもしばしばです。そういった個所も少しずつ修正して現在ではさすがに間違っている個所はないとは思いますが、まだまだ説明が足りなかったり文章が下手だったりという個所は目に付きます。

みなさんもこういった個所を目にしたり、よりよい説明の方法があると感じた場合には掲示板に書き込んで教えて下さい。初級講座をよりよいものとして行きたいと思います。

近年はVB6は既に過去の遺物となり、VB6を使用している方の大半は専門の業者ということになるでしょう。かつでは趣味でVB6を使っている方もたくさんいました。もちろん今でも趣味でVB6を使っている方もいるようです。現在2005年の10月ですが、このときにもいまだに仕事以外でVB6を使っている方が以外に多いのに驚かされます。それだけ「VB6」というソフトに価値があるということだと思います。

まもなくVB2005が発売されます。これはバージョンで言うとVB8ということになります。また、既にVB9の情報も私の耳に届いています。VB9では次のような構文がサポートされるようです。


Dim N As Integer = Select Count(Country) From Country In Countries Where Country.Population < 1000000

 

VB6からは想像もできないような構文です。

現在VB.NET未体験の方がいらっしゃいましたら是非VB2005を入手していじってみてください。その進化の急激さにはじめは戸惑うと思いますが、すぐに気に入ってもらえるようになると確信します。

やがてはVB6の技術や知識は古いものとなり、VB6を扱えるプログラマの人口はどんどん減少していくでしょう。しかし、みなさんがVB6で培ったスキルはVB2005で着実に生かすことができますし、本当に価値のあるVB6のスキルを獲得していればC#やDelphiなど他の言語においてもそのスキルを生かすことができるでしょう。

VB6で学んだみなさんのスキルが今度も発展していくことを切に願います。