[VB.NET][与太] なぜ VB6 は偉大だったのか

2015年1月1日

 いまだに、VB6 (旧Visual Basic) は偉大だったな、と感じていて、今だそれを超える処理系は Windows 系では出てきていないと思っている。
 ということで、偉大だなと思うその理由についてまとめてみる。

  1. VC++ の場合 (Visual Studio 2013)

     このボタンも何もない、ウインドウがあるだけのアプリを作るとします。

     Visual Studio が自動生成するコードは以下。182 行のスケルトンコードが生成されます。

    // Win32Project2.cpp : アプリケーションのエントリ ポイントを定義します。
    //
    
    #include "stdafx.h"
    #include "Win32Project2.h"
    
    #define MAX_LOADSTRING 100
    
    // グローバル変数:
    HINSTANCE hInst;								// 現在のインターフェイス
    TCHAR szTitle[MAX_LOADSTRING];					// タイトル バーのテキスト
    TCHAR szWindowClass[MAX_LOADSTRING];			// メイン ウィンドウ クラス名
    
    // このコード モジュールに含まれる関数の宣言を転送します:
    ATOM				MyRegisterClass(HINSTANCE hInstance);
    BOOL				InitInstance(HINSTANCE, int);
    LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);
    INT_PTR CALLBACK	About(HWND, UINT, WPARAM, LPARAM);
    
    int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                         _In_opt_ HINSTANCE hPrevInstance,
                         _In_ LPTSTR    lpCmdLine,
                         _In_ int       nCmdShow)
    {
    	UNREFERENCED_PARAMETER(hPrevInstance);
    	UNREFERENCED_PARAMETER(lpCmdLine);
    
     	// TODO: ここにコードを挿入してください。
    	MSG msg;
    	HACCEL hAccelTable;
    
    	// グローバル文字列を初期化しています。
    	LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    	LoadString(hInstance, IDC_WIN32PROJECT2, szWindowClass, MAX_LOADSTRING);
    	MyRegisterClass(hInstance);
    
    	// アプリケーションの初期化を実行します:
    	if (!InitInstance (hInstance, nCmdShow))
    	{
    		return FALSE;
    	}
    
    	hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WIN32PROJECT2));
    
    	// メイン メッセージ ループ:
    	while (GetMessage(&msg, NULL, 0, 0))
    	{
    		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
    		{
    			TranslateMessage(&msg);
    			DispatchMessage(&msg);
    		}
    	}
    
    	return (int) msg.wParam;
    }
    
    
    
    //
    //  関数: MyRegisterClass()
    //
    //  目的: ウィンドウ クラスを登録します。
    //
    ATOM MyRegisterClass(HINSTANCE hInstance)
    {
    	WNDCLASSEX wcex;
    
    	wcex.cbSize = sizeof(WNDCLASSEX);
    
    	wcex.style			= CS_HREDRAW | CS_VREDRAW;
    	wcex.lpfnWndProc	= WndProc;
    	wcex.cbClsExtra		= 0;
    	wcex.cbWndExtra		= 0;
    	wcex.hInstance		= hInstance;
    	wcex.hIcon			= LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32PROJECT2));
    	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
    	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
    	wcex.lpszMenuName	= MAKEINTRESOURCE(IDC_WIN32PROJECT2);
    	wcex.lpszClassName	= szWindowClass;
    	wcex.hIconSm		= LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
    
    	return RegisterClassEx(&wcex);
    }
    
    //
    //   関数: InitInstance(HINSTANCE, int)
    //
    //   目的: インスタンス ハンドルを保存して、メイン ウィンドウを作成します。
    //
    //   コメント:
    //
    //        この関数で、グローバル変数でインスタンス ハンドルを保存し、
    //        メイン プログラム ウィンドウを作成および表示します。
    //
    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
    {
       HWND hWnd;
    
       hInst = hInstance; // グローバル変数にインスタンス処理を格納します。
    
       hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
          CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
    
       if (!hWnd)
       {
          return FALSE;
       }
    
       ShowWindow(hWnd, nCmdShow);
       UpdateWindow(hWnd);
    
       return TRUE;
    }
    
    //
    //  関数: WndProc(HWND, UINT, WPARAM, LPARAM)
    //
    //  目的:    メイン ウィンドウのメッセージを処理します。
    //
    //  WM_COMMAND	- アプリケーション メニューの処理
    //  WM_PAINT	- メイン ウィンドウの描画
    //  WM_DESTROY	- 中止メッセージを表示して戻る
    //
    //
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    	int wmId, wmEvent;
    	PAINTSTRUCT ps;
    	HDC hdc;
    
    	switch (message)
    	{
    	case WM_COMMAND:
    		wmId    = LOWORD(wParam);
    		wmEvent = HIWORD(wParam);
    		// 選択されたメニューの解析:
    		switch (wmId)
    		{
    		case IDM_ABOUT:
    			DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
    			break;
    		case IDM_EXIT:
    			DestroyWindow(hWnd);
    			break;
    		default:
    			return DefWindowProc(hWnd, message, wParam, lParam);
    		}
    		break;
    	case WM_PAINT:
    		hdc = BeginPaint(hWnd, &ps);
    		// TODO: 描画コードをここに追加してください...
    		EndPaint(hWnd, &ps);
    		break;
    	case WM_DESTROY:
    		PostQuitMessage(0);
    		break;
    	default:
    		return DefWindowProc(hWnd, message, wParam, lParam);
    	}
    	return 0;
    }
    
    // バージョン情報ボックスのメッセージ ハンドラーです。
    INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
    {
    	UNREFERENCED_PARAMETER(lParam);
    	switch (message)
    	{
    	case WM_INITDIALOG:
    		return (INT_PTR)TRUE;
    
    	case WM_COMMAND:
    		if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
    		{
    			EndDialog(hDlg, LOWORD(wParam));
    			return (INT_PTR)TRUE;
    		}
    		break;
    	}
    	return (INT_PTR)FALSE;
    }
    

     また、上記のメインコードのほかに自動生成されるファイル群は以下。かなり多くのファイルが生成されます。

  2. VB2003 の場合

     VB2003 でボタンを一つ置いただけのアプリを作るとします。

     Visual Studio 2003 が自動生成するコードは以下。61 行のスケルトンコードが生成されます。
     うち、ボタン部分のコードは最後のほうの3行だけですから、ボタンに関係ないコードが 58 行生成される、ともいえます。

    Public Class Form1
        Inherits System.Windows.Forms.Form
    
    #Region " Windows フォーム デザイナで生成されたコード "
    
        Public Sub New()
            MyBase.New()
    
            ' この呼び出しは Windows フォーム デザイナで必要です。
            InitializeComponent()
    
            ' InitializeComponent() 呼び出しの後に初期化を追加します。
    
        End Sub
    
        ' Form は、コンポーネント一覧に後処理を実行するために dispose をオーバーライドします。
        Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
            If disposing Then
                If Not (components Is Nothing) Then
                    components.Dispose()
                End If
            End If
            MyBase.Dispose(disposing)
        End Sub
    
        ' Windows フォーム デザイナで必要です。
        Private components As System.ComponentModel.IContainer
    
        ' メモ : 以下のプロシージャは、Windows フォーム デザイナで必要です。
        'Windows フォーム デザイナを使って変更してください。  
        ' コード エディタを使って変更しないでください。
        Friend WithEvents Button1 As System.Windows.Forms.Button
         Private Sub InitializeComponent()
            Me.Button1 = New System.Windows.Forms.Button
            Me.SuspendLayout()
            '
            'Button1
            '
            Me.Button1.Location = New System.Drawing.Point(176, 16)
            Me.Button1.Name = "Button1"
            Me.Button1.Size = New System.Drawing.Size(80, 40)
            Me.Button1.TabIndex = 0
            Me.Button1.Text = "Button1"
            '
            'Form1
            '
            Me.AutoScaleBaseSize = New System.Drawing.Size(5, 12)
            Me.ClientSize = New System.Drawing.Size(292, 273)
            Me.Controls.Add(Me.Button1)
            Me.Name = "Form1"
            Me.Text = "Form1"
            Me.ResumeLayout(False)
    
        End Sub
    
    #End Region
    
        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    
        End Sub
    End Class
    

     それでも C++ の例に比べるとかなりコード量が減っており、また #Region ~ #End Region は普段折り畳んでいるから、目に見える部分はさらに少なくなります。
     とはいえ、折り畳みを解除すれば自動生成コードは容易に見えてしまいます。

  3. VB2005 以降の場合

     VB2003 以前では #Region で自動生成コードを隠すようにしていました。が、恐らくこれは不興で、VB.NET 採用を敬遠する理由の一つになっていた気がします。
     それを解決するため、VB2005 以降では Partial Class という機能 (=同一クラスを複数のファイルに記述できる仕組み) が実装されました。この機能の登場により VB2003 で #Region で囲われた部分が別のファイルに移されました。したがって、画面のコードとしては、ほぼボタン関係のコードのみが自動生成されたように見えます。

     かつて #Region で囲われた部分 は Form1.Designer.vb に移動しましたがそのファイルはデフォルトでは表示されないようになっています。

  4. VB6 の場合

     そもそも VB6 はどうだったかというと、こんなボタンが一つだけの画面の場合。

     コードはこうでした。

     VB2005 以降でも残っていた、Class ~ End Class の記述もなく、ボタンのコードしかありません。

     ボタンに関係ないコードの生成は 0 行です。また構成されるファイルも Form1 ただひとつしかないように見えます。じつにシンプルです。ファイルも "Form1" 用のファイルが一つだけあるかのように見えます。( 実際には form1.frm ファイルとは別にプロジェクトファイルがありますが。)

     VB6 の場合、プロジェクト作成直後の自動生成コードは 0 行です。「プロジェクトを作ったらたくさんコードが生成された」ということはありません。あくまで「このコンポーネントのイベントプロシージャを作りたい」という操作の後にコード生成されます。そうして自動生成されたコードもイベントプロシージャのブロック (Sub と End Sub) だけですから、VB6 で書かれているコードは、ほぼ "プログラマー自身が書いた" コードです。

  5. Windows 8 ストアアプリの場合

     ところが Windows 8 ストアアプリになると、C++ の WinForm と同じような状況を呈してきます。

     まず、プロジェクト作成時点で表示されるのは画面 (Mainpage.xaml) ではなくて、app.xaml.vb です。これが 56 行あり、これまでの from デザイナを見慣れていると、いきなりコードを突きつけられるため、結構な威圧感があります。


    ' 空のアプリケーション テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234227 を参照してください
    
    ''' <summary>
    ''' 既定の Application クラスを補完するアプリケーション固有の動作を提供します。
    ''' </summary>
    NotInheritable Class App
        Inherits Application
    
        ''' <summary>
        ''' アプリケーションがエンド ユーザーによって正常に起動されたときに呼び出されます。他のエントリ ポイントは、
        ''' アプリケーションが特定のファイルを開くために呼び出されたときに
        ''' 検索結果やその他の情報を表示するために使用されます。
        ''' </summary>
        ''' <param name="args">起動要求とプロセスの詳細を表示します。</param>
        Protected Overrides Sub OnLaunched(args As Windows.ApplicationModel.Activation.LaunchActivatedEventArgs)
            Dim rootFrame As Frame = Window.Current.Content
    
            ' ウィンドウに既にコンテンツが表示されている場合は、アプリケーションの初期化を繰り返さずに、
            ' ウィンドウがアクティブであることだけを確認してください
    
            If rootFrame Is Nothing Then
                ' ナビゲーション コンテキストとして動作するフレームを作成し、最初のページに移動します
                rootFrame = New Frame()
                If args.PreviousExecutionState = ApplicationExecutionState.Terminated Then
                    'TODO: 以前中断したアプリケーションから状態を読み込みます。
                End If
                ' フレームを現在のウィンドウに配置します
                Window.Current.Content = rootFrame
            End If
            If rootFrame.Content Is Nothing Then
                ' ナビゲーション スタックが復元されていない場合、最初のページに移動します。
                ' このとき、必要な情報をナビゲーション パラメーターとして渡して、新しいページを
                ' 構成します
                If Not rootFrame.Navigate(GetType(MainPage), args.Arguments) Then
                    Throw New Exception("Failed to create initial page")
                End If
            End If
    
            ' 現在のウィンドウがアクティブであることを確認します
            Window.Current.Activate()
        End Sub
    
        ''' <summary>
        ''' アプリケーションの実行が中断されたときに呼び出されます。アプリケーションの状態は、
        ''' アプリケーションが終了されるのか、メモリの内容がそのままで再開されるのか
        ''' わからない状態で保存されます。
        ''' </summary>
        ''' <param name="sender">中断要求の送信元。</param>
        ''' <param name="e">中断要求の詳細。</param>
        Private Sub OnSuspending(sender As Object, e As SuspendingEventArgs) Handles Me.Suspending
            Dim deferral As SuspendingDeferral = e.SuspendingOperation.GetDeferral()
            ' TODO: アプリケーションの状態を保存してバックグラウンドの動作があれば停止します
            deferral.Complete()
        End Sub
    
    End Class
    

     次に「空のアプリケーション 」テンプレートを選択した場合でも、複数のプロジェクトファイル生成されています。
     画面用のファイルがどれであるかを特定するだけでも、VB6 に比べれば困難です。

     ただ MainPage.xaml.vb 自体は、コメントや Inherits 他の自動生成コードがあるとはいえ、WinForm に比べて多量のコードが生成されるわけではありません。この点は VB2005 以降の WinForm とあまり変わりません。(Inherits Page が余計ですが)

    ' 空白ページのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234238 を参照してください
    
    ''' <summary>
    ''' それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    ''' </summary>
    Public NotInheritable Class MainPage
        Inherits Page
    
        Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
    
        End Sub
    End Class
    

     しかし、Win 8 ストアアプリの場合、Win Form と同じように適当にボタンを配置していって・・・というスタイルでアプリを作っていけるかというと必ずしもそうではありません。
     ストアアプリらしい(=Microsoftのデザインガイドに従った)アプリを作ろうとすると、上記の「空のアプリケーション」か作成するのは困難です。その場合、いくつかの「ハブアプリケーション」や「グリッドアプリケーション」のようなテンプレートが用意されており、それを選択することでストアアプリ用のフレームワークを使用することができます。

     しかし、そのテンプレートを選択した場合、やはりファイルやコードが大量に自動生成されるわけです。以下は「グリッドアプリケーション」テンプレートを選択したときの GroupedItemsPage.xaml.vb ですが、103 行のコードが自動生成されます。
     103 行のうち、ほとんどはコメントと空行で、それらを除けば正味 30 行ほどです。内容も プロパティ×2, コンストラクタ(New)×1, プロシージャ×5 であり、プロシージャの内容も 2 行程度ではあるのです。しかし、使用するためにはその意味を知る必要があり、その点で学習コストが高くなってしまっています。

    ' グループ化されたアイテム ページのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234231 を参照してください
    
    ''' <summary>
    ''' グループ化されたアイテムのコレクションを表示するページです。
    ''' </summary>
    Public NotInheritable Class GroupedItemsPage
        Inherits Page
    
        ''' <summary>
        ''' NavigationHelper は、ナビゲーションおよびプロセス継続時間管理を
        ''' 支援するために、各ページで使用します。
        ''' </summary>
        Public ReadOnly Property NavigationHelper As Common.NavigationHelper
            Get
                Return Me._navigationHelper
            End Get
        End Property
        Private _navigationHelper As Common.NavigationHelper
    
        ''' <summary>
        ''' これは厳密に型指定されたビュー モデルに変更できます。
        ''' </summary>
        Public ReadOnly Property DefaultViewModel As Common.ObservableDictionary
            Get
                Return Me._defaultViewModel
            End Get
        End Property
        Private _defaultViewModel As New Common.ObservableDictionary()
    
    
        Public Sub New()
            InitializeComponent()
            Me._navigationHelper = New Common.NavigationHelper(Me)
            AddHandler Me._navigationHelper.LoadState,
                AddressOf NavigationHelper_LoadState
        End Sub
    
        ''' <summary>
        ''' このページには、移動中に渡されるコンテンツを設定します。前のセッションからページを
        ''' 再作成する場合は、保存状態も指定されます。
        ''' </summary>
        ''' <param name="sender">
        ''' イベントのソース (通常、<see cref="NavigationHelper"/>)
        ''' </param>
        ''' <param name="e">このページが最初に要求されたときに
        ''' <see cref="Frame.Navigate"/> に渡されたナビゲーション パラメーターと、
        ''' 前のセッションでこのページによって保存された状態の辞書を提供する
        ''' イベント データ。ページに初めてアクセスするとき、状態は null になります。</param>
        Private Async Sub NavigationHelper_LoadState(sender As Object, e As Common.LoadStateEventArgs)
            ' TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
            Dim sampleDataGroups As IEnumerable(Of Data.SampleDataGroup) = Await Data.SampleDataSource.GetGroupsAsync()
            Me.DefaultViewModel("Groups") = sampleDataGroups
        End Sub
        ''' <summary>
        ''' グループ ヘッダーがクリックされたときに呼び出されます。
        ''' </summary>
        ''' <param name="sender">ボタンは、選択されたグループのグループ ヘッダーとして使用されます。</param>
        ''' <param name="e">クリックがどのように開始されたかを説明するイベント データ。</param>
        Private Sub Header_Click(sender As Object, e As RoutedEventArgs)
    
            ' ボタン インスタンスがどのグループを表すかを確認します
            Dim group As Object = DirectCast(sender, FrameworkElement).DataContext
    
            ' 適切な移動先のページに移動し、新しいページを構成します。
            ' このとき、必要な情報をナビゲーション パラメーターとして渡します
            Me.Frame.Navigate(GetType(GroupDetailPage), DirectCast(group, Data.SampleDataGroup).UniqueId)
        End Sub
    
        ''' <summary>
        ''' グループ内のアイテムがクリックされたときに呼び出されます。
        ''' </summary>
        ''' <param name="sender">クリックされたアイテムを表示する GridView (アプリケーションがスナップ
        ''' されている場合は ListView) です。</param>
        ''' <param name="e">クリックされたアイテムを説明するイベント データ。</param>
        Private Sub ItemView_ItemClick(sender As Object, e As ItemClickEventArgs)
    
            ' 適切な移動先のページに移動し、新しいページを構成します。
            ' このとき、必要な情報をナビゲーション パラメーターとして渡します
            Dim itemId As String = DirectCast(e.ClickedItem, Data.SampleDataItem).UniqueId
            Me.Frame.Navigate(GetType(ItemDetailPage), itemId)
        End Sub
    
    #Region "NavigationHelper の登録"
    
        ''' このセクションに示したメソッドは、NavigationHelper がページの
        ''' ナビゲーション メソッドに応答できるようにするためにのみ使用します。
        ''' 
        ''' ページ固有のロジックは、
        ''' <see cref="Common.NavigationHelper.LoadState"/>
        ''' および <see cref="Common.NavigationHelper.SaveState"/> のイベント ハンドラーに配置する必要があります。
        ''' LoadState メソッドでは、前のセッションで保存されたページの状態に加え、
        ''' ナビゲーション パラメーターを使用できます。
    
        Protected Overrides Sub OnNavigatedTo(e As NavigationEventArgs)
            _navigationHelper.OnNavigatedTo(e)
        End Sub
    
        Protected Overrides Sub OnNavigatedFrom(e As NavigationEventArgs)
            _navigationHelper.OnNavigatedFrom(e)
        End Sub
    #End Region
    End Class
    
  6. 結論

     そういったわけで、いまだに VB6 に変わるフレームワーク、というか開発環境が出てきていないな、といった感想。

     自動生成コードが大きい、というのは学習コストを高め、敬遠する要因を作っている気がします。
     また、ここでは触れいてませんが、WPF をはじめとした XAML 処理系は小さな機能を組み合わせることで一つの機能を作り出している、という点においては美しい設計だと思います。しかし反面、VB6 で難なくできていたことができなかったり、しようと思ったときに xaml の内部構造を掌握する必要が出てきたりなど、やはり学習コストで辛い面があります。

     Windows 8 ストアアプリを普及させるためには、VB6 程度にライトな(=内部実装を隠しまくった)開発環境の導入が必要なのではないかな?と個人的には思ったりはしています。さて今後どうなるのか・・・






カテゴリー: IT / テクノロジー, Program, VB.NET, 与太話

Follow comments via the RSS Feed | Leave a comment | Trackback URL

コメントを投稿する

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


«   »
 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org