2010.11.23.
石立 喬

Visual C++ 2010 Express の易しい使い方(19)

――― Mandelbrot図形の一部をクリックして、対応する条件でJulia図形を描く―――

 Mandelbrot(マンデルブロー)集合(「易しい使い方(12)」で紹介)の図形にすっかりはまってしまって、その親戚関係に当たるJulia集合図形も手がけてみることにした。すでに「易しい使い方(12)」で説明した部分は一部省略されているので、そちらも参照して欲しい。高速化を図るため、標準の複素数関数は使用しない。

Julia集合とは
 フランスの数学者Gaston Maurice Juliaが1918年(Benoit Mandelbrotは1924年生まれであるから、Mandelbrotの生まれる前)に論文発表したものであるが、当時は計算時間が非常に長く掛かることから、実際には永らく図形化されていなかった。 Mandelbrot集合が先に注目され、次いでJulia集合も見直された。Julia集合がMandelbrot集合と違うところは、Z=Z2+Cの繰り返し計算において、Mandelbrot集合がZの初期値Z0を常にZ0=0+0iとしてC平面状にCを変化さて図形を描画したのに対し、Julia集合では、Cを固定してZ0を変化させ、Z0平面上に図形を描画する。何回繰り返すとZの絶対値が一定の値に達するか、またはあらかじめ設定した回数では到達しないか(ゼロに収束してしまう場合もある)を調べて、繰り返し回数を色で表現するところはMandelbrot集合と同じである。Julia集合を図形化するには、CのみならずZ0の初期値も外部から与える必要がある。

Julia集合の画像の作成方法
 パソコン画面上の座標を(x,y)としたとき、
  zr(Z0の実数部)=x*step(ピクセル当りの変化分)+z0real(Z0の実数部の中心)
  zi(Z0の虚数部)=y*step(ピクセル当りの変化分)+z0imag(Z0の虚数部の中心)
で与える。
 step(ピクセル当りの変化分)は、xとyに対して同じ値を使用する。stepを大きくすると、画面上でのJulia図形が縮小され、小さくすると部分が拡大される。繰り返し回数を、どのような色の系列で表示するかによって、図形の感じが全く変わったものになる。

高速化の工夫
 Julia図形の描画には、非常に時間がかかる場合がある。なかなか発散しない条件で発散を確認するためには、最大繰り返し回数(count_max)を上げて実行する必要がある。
 ZとCを共に複素数として、Z=Z2+Cを計算するには、Zの実数部をzr、虚数部をzi、Cの実数部をci、Cの虚数部をciとして、標準の複素数関数を使うことなく、下記のように直接に計算する。
  新しいzr=zr*zr-zi*zi+cr ----------------- (1)
  新しいzi=2*zr*zi+ci --------------------- (2)
  新しいZの絶対値の平方=zr*zr+zi*zi ------- (3)
ただし、(1)式で得た結果の新しいzrをすぐ(2)式に入れることはできないので、一旦tempに保存しておく。これらの計算は、乗算6回、加減算4回である。
 そこで、zr2=zr*zr、zi2=zi*ziをあらかじめ計算しておく方法を用いると、
  zr2=zr*zr  ------------------------ (1)
  zi2=zi*zi --------------------------- (2)
  新しいzr=zr2-zi2+cr ----------------- (3)
  新しいzi=2*zr*zi+ci ----------------- (4)
  新しいZの絶対値の平方=zr2+zi2 ------- (5)
となって、乗算4回、加減算4回となり、乗算回数が2回減る。

プログラムの使用方法
1)プログラムを起動させると、「Mandelbrotモード」になっており、Mandelbrot図形が表示される。
2)Mandelbrot図形上でマウスを移動させると、その位置に対応するCの実数部(creal)、Cの虚数部(cimag)が右側に表示される。
3)Mandelbrot図形上の希望する位置でマウスをクリックすると、対応するcrealとcimagが設定され、「Juliaモード」に変わる。以後、再び「Mandelbrotモード」に戻ると、Mandelbrot図形上に設定された新しいCの位置が白い十字で示される。
4)表示されたJulia図形は、設定されたcrealとcimagを使用しているが、Z0の中心の実数部(z0real)、Z0の中心の虚数部(z0imag)は共に0.0に初期設定され、ピクセル当りの変化量(step)は0.006に初期設定されている。
5)Julia図形上でマウスを移動させると、対応するZ0の実数部(z0real)と虚数部(z0imag)が右側に表示される。
6)Julia図形上の希望する位置でマウスをクリックすると、対応する場所が新しいz0realとz0imagとして設定されて、図形の中央に移動し、stepが1/2になり(図形が拡大される)。
7)Julia図形上のクリックを一回だけ元に戻すことができ、その場合は「戻す」ボタンをクリックする。
8)希望する各データを直接入力したい場合には、画面左上の「条件設定」ボタンで「条件の設定」ダイアログボックスが開くので、そこに入力する。「設定」ボタンをクリックすると、ダイアログボックスが閉じる。
9)ダイアログボックスが閉じても、Julia図形のすべてが再描画されないので、「再描画」ボタンをクリックして再描画する。

プログラムの説明
 creal、cimagなどの各データは、FormDialogに設定されているものを参照し、Form1.h内ではFormDialog::crealなどを呼び出して使用する。ただし、Julia図形の描画は繰り返し回数が多いので、高速化のためにローカルにコピーして使用する。
◎Form1_Load()
1)ラジオボタン、「戻る」ボタンの初期設定をする。
2)creal、cimagなどの各データを初期設定する(FormDialog::crealなどに初期値を入れる)。
3)Mandelbrot図形を作成し、bmap_mandelとして保存する(以後、再作成は行わない)。
◎Form1_Paint()
[Mandelbrotモードの場合]
1)bmap_mandelを描画する。
2)その時に設定されているcrealとcimagの位置をMandelbrot図形上に白い十字で表示する。
[Juliaモードの場合]
1)FormDialogから 各データを読み込み、その条件によるJulia図形を作成して描画する。
2)各データを下部に表示する。
◎Form1_MouseMove()
 図形描画領域内の場合は下記による。
[Mandelbrotモードの場合]
1)マウス位置から、対応するCの実数部の値(cr)、Cの虚数部の値(ci)を計算する。
2)crとciを画面右方に表示する。
[Juliaモードの場合]
1)マウス位置から、対応するZ0の実数部の値(zr)、Z0の虚数部の値(zi)を計算する。
2)zrとziを画面右方に表示する。
3)図形描画領域外の場合は、それまでに文字が表示されている場合があるので、表示領域を消す。
◎Form1_MouseDown()
[Mandelbrotモードの場合]
1)Mandelbrot図形上のクリックした位置からcrealとcimagを求め、設定する。
2)z0real、z0imag、stepを初期設定する。
3)Juliaモードに切り替える。
[Juliaモードの場合]
1)それまでのz0real、z0imag、stepを、z0real_old、z0imag_old、step_oldに保存する。
2)マウス位置から計算してz0real、z0imagを更新する。stepは1/2に更新する。
3)buttonUndo(「戻る」ボタン)を有効にする。
4)再描画する。
◎buttonUndo_Click()
1)z0real_old、z0imag_old、step_oldをz0real、z0imag、stepに代入する。
2)buttonUndoを無効にする。
3)再描画する。
◎countToColor()、NumberToRGBColor()
これは、「易しい使い方(12)」で用いたものと同じである。

プログラム
 主な変数の役割は次の通り。
  creal --------- 外部から与えるCの実数部の値
  cimag --------- 外部から与えるCの虚数部の値
  z0real -------- Z0(Zの初期値)の実数部で、表示するJulia画像のx座標の中心部に当たる。
  z0imag -------- Z0(Zの初期値)の虚数部で、表示するJulia画像のy座標の中心部に当たる。
  step ---------- xまたはyが増加する度にZ0の実数部または虚数部の値が変化する量で、小さい程、表示される画像は拡大される。
  value --------- Zの絶対値の平方で、これが4.0未満であれば発散していないと判断し、演算の繰り返しを継続する。
  count_max ----- 外部から与える繰り返し回数の上限である。大きすぎると計算時間が長くかかり、小さくするとvalueが4.0に達しない前に計算を打ち切ることになる。
  color_number -- 繰り返し回数を表示するための階調数である。

Form1.h
#pragma once
#include "FormDialog.h"

static int X0=10,Y0=40; //図形の表示位置
static int X1=10,Y1=450; //設定条件の表示位置
static int X2=430,Y2=35; //マウス移動位置表示
static int SIZE=200;

int count_max,color_number;
float z0real_old,z0imag_old,step_old;  //元に戻すための記憶場所
static float MANDEL_CREAL=-0.5f;
static float MANDEL_CIMAG=0.f0;
static float MANDEL_STEP=0.006f;
static Rectangle^ rectangle1=gcnew Rectangle(X0,Y0,SIZE*2,SIZE*2);  //MandelbrotまたはJulia図形の領域
static Bitmap^ bmap_mandel=gcnew Bitmap(SIZE*2,SIZE*2);

private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {

   radioMandel->Checked=true;  //最初はMandelbrotモードなので、radioMandelにチェック
   radioJulia->Checked=false;  //最初はMandelbrotモードなので、radioJuliaはチェックしない
   buttonUndo->Enabled=false;  //クリックによる座標指定がされるまで、buttonUndoは不要

   int MANDEL_COUNT_MAX=1024;
   int MANDEL_COLOR_NUMBER=64;

   int count,x,y;
   float cr,ci,zr,zi,temp,value;

   //各種データーを初期設定
   FormDialog::creal=0.0f;
   FormDialog::cimag=0.0f;
   FormDialog::z0real=0.0f;
   FormDialog::z0imag=0.0f;
   FormDialog::step=0.006f;
   FormDialog::count_max=256;
   FormDialog::color_number=64;

   //Mandelbrot図形を作成して、bmap_mandelを用意する
   for(x=-SIZE;x<SIZE;x++){
      cr=x*MANDEL_STEP+MANDEL_CREAL;
      for(y=-SIZE;y<SIZE;y++){
         ci=y*MANDEL_STEP+MANDEL_CIMAG;
         zr=0.0f;
         zi=0.0f;
         value=0.0f;
         count=0;
         do{
            temp=zr*zr-zi*zi+cr;    //Zの二乗の実数部
            zi=2*zr*zi+ci;       //Zの二乗の虚数部
            zr=temp;
            value=zr*zr+zi*zi;     //Zの絶対値の平方
            count++;
            if(count>MANDEL_COUNT_MAX){
               count=-1;
               break;
            }
         }while(value<4.0f);
         bmap_mandel->SetPixel(SIZE+x,SIZE-1-y,countToColor(count,MANDEL_COLOR_NUMBER));
      }
   }

}

private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {

   Graphics^ gr=e->Graphics;
   Bitmap^ bmap=gcnew Bitmap(SIZE*2,SIZE*2);
   String^ string1;

   int count,x,y;
   double creal,cimag,z0real,z0imag,step;
   double zr,zi,zr2,zi2,zreal,temp,value;

   if(radioMandel->Checked){   //Mandelbrotモード
      gr->DrawImage(bmap_mandel,X0,Y0);
      x=(FormDialog::creal-MANDEL_CREAL)/MANDEL_STEP+SIZE;
      y=-(FormDialog::cimag-MANDEL_CIMAG)/MANDEL_STEP+SIZE;
      //「+」図形を白で描く
      gr->DrawLine(Pens::White,X0+x-5,Y0+y,X0+x+5,Y0+y);
      gr->DrawLine(Pens::White,X0+x,Y0+y-5,X0+x,Y0+y+5);
   }
   if(radioJulia->Checked){    //Juliaモード
      creal=FormDialog::creal;
      cimag=FormDialog::cimag;
      z0real=FormDialog::z0real;
      z0imag=FormDialog::z0imag;
      step=FormDialog::step;
      count_max=FormDialog::count_max;
      color_number=FormDialog::color_number;

      //Julia図形を作成する
      for(x=-SIZE;x<SIZE;x++){
         zreal=x*step+z0real;
         for(y=-SIZE;y<SIZE;y++){
            zr=zreal;
            zi=y*step+z0imag;
            count=0;
            do{
               zr2=zr*zr;
               zi2=zi*zi;
               temp=zr2-zi2+creal;   //Zの二乗の実数部
               zi=2*zr*zi+cimag;    //Zの二乗の虚数部
               zr=temp;
               value=zr2+zi2;      //Zの絶対値の平方
               count++;
               if(count>count_max){//発散しなかった
                  count=-1;
                  break;
               }
            }while(value<4.0f);
            bmap->SetPixel(SIZE+x,SIZE-1-y,countToColor1(count,color_number));
         }
      }
      
      gr->DrawImage(bmap,X0,Y0);
      string1=String::Format("Cの実数部の中心={0}、Cの虚数部の中心={1}",creal,cimag);
      gr->DrawString(string1,Font,Brushes::Black,X1,Y1);
      string1=String::Format("Z0の実数部の中心={0}、Z0の虚数部の中心={1}",z0real,z0imag);
      gr->DrawString(string1,Font,Brushes::Black,X1,Y1+20);
      string1=String::Format("ステップ/ピクセル={0}、最大繰返し回数={1}、表示色の階調{2}",step,count_max,color_number);
      gr->DrawString(string1,Font,Brushes::Black,X1,Y1+40);
   }

}

private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int x,y;
   float cr,cizr,zi;
   String^ string1;

   if(rectangle1->Contains(e->X,e->Y)){     //図形の範囲内
      x=e->X-X0;
      y=e->Y-Y0;
      if(radioMandel->Checked){   //Mandelbrotモード
         cr=(x-SIZE)*MANDEL_STEP+MANDEL_CREAL;
         ci=-(y-SIZE)*MANDEL_STEP+MANDEL_CIMAG;
         g->FillRectangle(Brushes::White,415,95,110,40);
         string1=String::Format("Cの実部={0:F3}",cr);
         g->DrawString(string1,Font,Brushes::Black,420,100);
         string1=String::Format("Cの虚部={0:F3}",ci);
         g->DrawString(string1,Font,Brushes::Black,420,120);
      }
      if(radioJulia->Checked){    //Juliaモード
         zr=(x-SIZE)*FormDialog::step+FormDialog::z0real;
         zi=-(y-SIZE)*FormDialog::step+FormDialog::z0imag;
         g->FillRectangle(Brushes::White,415,95,110,40);
         string1=String::Format("Z0の実部={0:F3}",zr);
         g->DrawString(string1,Font,Brushes::Black,420,100);
         string1=String::Format("Z0の虚部={0:F3}",zi);
         g->DrawString(string1,Font,Brushes::Black,420,120);
      }
   }
   else g->FillRectangle(Brushes::White,415,95,110,40);

}

private: System::Void Form1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int x,y;

   if(rectangle1->Contains(e->X,e->Y)){   //図形の範囲内
      x=e->X-X0;
      y=e->Y-Y0;
      if(radioMandel->Checked){   //Mandelbrotモード

         //Mandelbrot図形からcrealとcimagを取り込み設定する
         FormDialog::creal=(x-SIZE)*MANDEL_STEP+MANDEL_CREAL;
         FormDialog::cimag=-(y-SIZE)*MANDEL_STEP+MANDEL_CIMAG;

         //Julia図形のz0とstepを初期設定
         FormDialog::z0real=0.0f;
         FormDialog::z0imag=0.0f;
         FormDialog::step=0.006f;

         radioJulia->Checked=true;
         radioMandel->Checked=false;

      }
      else{                 //Juliaモード

         //それまでのz0real,z0imag,stepを保存する
         z0real_old=FormDialog::z0real;
         z0imag_old=FormDialog::z0imag;
         step_old=FormDialog::step;

         //クリックした場所を新しいz0realとz0imagとする
         FormDialog::z0real=(x-SIZE)*FormDialog::step+FormDialog::z0real;
         FormDialog::z0imag=-(y-SIZE)*FormDialog::step+FormDialog::z0imag;

         //stepを1/2にする(図形を2倍に拡大する)
         FormDialog::step/=2;

         //buttonUndoを使用できるようにする
         buttonUndo->Enabled=true;
      }
      Invalidate();
   }

}

private: System::Void buttonSettings_Click(System::Object^ sender, System::EventArgs^ e) {

   FormDialog^ dlg=gcnew FormDialog();
   dlg->ShowDialog();

}

private: System::Void buttonUndo_Click(System::Object^ sender, System::EventArgs^ e) {

   FormDialog::z0real=z0real_old;
   FormDialog::z0imag=z0imag_old;
   FormDialog::step=step_old;
   buttonUndo->Enabled=false;
   Invalidate();

}

private: System::Void buttonUpdate_Click(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

private: System::Void radioMandel_CheckedChanged(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

private: System::Void radioJulia_CheckedChanged(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

//カウント数を色に変換するメソッド
private: Color countToColor(int n,int base){

   if(n<0) return Color::Black;  //Zの絶対値が一定値を超えなかったときは黒を返す
   else{
      int d=n % base;
      d*=(256/base);
      return NumberToRGBColor(d);
   }

}

//数値をColorに変換するメソッド
private: Color NumberToRGBColor(int n){

   int h=n/43;
   int r,g,b;
   switch(h){
      case 0:r=255;    g=n*6;    b=0;      break;   //n= 0~ 42
      case 1:r=(85-n)*6; g=255;    b=0;      break;   //n= 43~ 85
      case 2:r=0;     g=255;    b=(n-86)*6; break;   //n= 86~128
      case 3:r=0;     g=(171-n)*6; b=255;    break;   //n=129~171
      case 4:r=(n-172)*6; g=0;     b=255;    break;   //n=172~214
      case 5:r=255;    g=0;     b=(255-n)*6; break;   //n=215~255
   };
   return Color::FromArgb(r,g,b);

}


FormDialog.h

public:
   static float creal,cimag,z0real,z0imag,step;
   static int count_max,color_number;

private: System::Void FormDialog_Load(System::Object^ sender, System::EventArgs^ e) {

   //各種データをテキストボックスに表示する
   textBox1->Text=creal.ToString();
   textBox2->Text=cimag.ToString();
   textBox3->Text=z0real.ToString();
   textBox4->Text=z0imag.ToString();
   textBox5->Text=step.ToString();
   textBox6->Text=count_max.ToString();
   textBox7->Text=color_number.ToString();

}

private: System::Void buttonSet_Click(System::Object^ sender, System::EventArgs^ e) {

   //テキストボックスの内容を各種データに変換する
   creal=float::Parse(textBox1->Text);
   cimag=float::Parse(textBox2->Text);
   z0real=float::Parse(textBox3->Text);
   z0imag=float::Parse(textBox4->Text);
   step=float::Parse(textBox5->Text);
   count_max=int::Parse(textBox6->Text);
   color_number=int::Parse(textBox7->Text);
   this->Close();

}

private: System::Void buttonCancel_Click(System::Object^ sender, System::EventArgs^ e) {

   this->Close();

}

得られた画面
 図1は、プログラム起動時の画面である。最初は「Mandelbrotモード」になっていて、Mandelbrot図形が表示されている。図形上に、creal=0、cimag=0に相当する点が、白い十字で表示されている。図形の中心は、MANDEL_CREAL=-0.5、MANDEL_CIMAG=0により設定されているので、点は図形の右寄りで、上下方向には中心にある。
 Mandelbrot図形の右には、Cの実(数)部、Cの虚(数)部が表示されているが、たまたま置かれていたマウスの位置を示していて、特に意味は無い。
 マウスクリックによるJulia図形の変更が行われていないので、「戻す」ボタンは無効になっている。



図1 起動直後の画面


 図2は「条件の設定」ダイアログボックスを示したもので、図3は、それによって得られたJulia図形を示す。ダイアログボックスでデータを指定した場合には、「戻す」ボタンが無効になっている。


図2 キー入力によって各データを設定したダイアログボックス



図3 ダイアログボックスで設定された条件によるJulia図形


子フォームの使い方
 すでに、「易しい使い方(4)」で説明してあるが、念のために簡単に解説する。
◎新しいフォーム(子フォーム)を追加する方法
1) 統合開発環境の「ソリューション エクスプローラ」(現れていない場合は、メニューから、「表示」→「ソリューション エクスプローラ」)で、プロジェクトのトップ(たとえば「Julia」など)を右クリックし、「追加」→「新しい項目」を選択する。
2)「新しい項目の追加」ウインドウが開くので、左の欄から「UI(User Interfaceの意)」、右の欄から 「Windowsフォーム」を選び、「名前」を「FormDialog」(一例)と入力して、「追加」ボタンをクリックする。
3)FormDialog.cpp と FormDialog.h が生成され、FormDialog の「FormDialog.h[デザイン]」画面が現れる。
4)フォームの中央で右クリックして、「プロパティ」ウインドウで下記のように設定する。
  「表示」欄で、
      FormBorderStyle --- FixedDialog (アイコンが無く、サイズの変更ができない)
      Text -------------- 条件の設定(これは一例で、キャプションを付ける)
  「ウインドウスタイル」欄で、
      MaximizeBox ------- False (最大化ボタンを消す)
      MinimizeBox ------- False (最小化ボタンを消す)
 「Text」項目以外は、項目を選択すると、右側に下向きの三角矢印が現れるので、これをクリックし、選択する。
◎親フォームのボタンから子フォームをモーダル・ダイアログボックスとして呼び出す方法
 モーダルとは、子フォームを閉じるまで、親フォームでの実行が出来ないことで、そうでないものにモードレスがある。特に目的が無い限り、一般的にはモーダルが用いられる。モーダルを用いると、子フォームが閉じられると同時に、親フォームに制御が戻るので、子フォームからの情報の転送に便利である。
1) 親フォームで、「条件設定」ボタン(一例、子フォーム呼び出しのために作成したボタン)をダブルクリックする。
2)buttonDialog_Click() メソッドが現れるので、
      FormDialog^ dlg=gcnew FormDialog();
      dlg->ShowDialog();
と記述する。ただし、FormDialog は、さきに命名した、開きたい子フォームの名称である。子フォームからの入力情報を受け取るには、この下に続けてコードを記入する。
3)親フォームの Form1.h の最初の部分(#pragma onceの後)に
      #include “FormDialog.h”
  と記述する。
◎子フォームで設定したデータを親フォームから使う方法
1)子フォームで作成される変数をfloat型で crealとすると、子フォームのFormDialog.h において、
      public: static real creal;
などと宣言しておく。 public でなく、private にすると、親フォームクラスから呼び出せなくなり、static を省くと親フォームに渡せなくなって、いずれもエラーになってビルドできない。
2)親フォームクラスで子フォームクラスの変数を呼び出すには、
     FormDialog::creal
などを用いる。


「Visual C++ の勉強部屋」(目次)へ