MFC アプリケーション ウィザードは、ドキュメント クラスとビュー クラスを使用してアプリケーション スケルトンを作成します。 MFC により、データ管理がこれらの 2 つのクラスに分離されます。
ドキュメント
データが格納され、データの複数のビューの更新が調整されます。
ビュー
データが表示され、選択や編集などのユーザー操作が管理されます。
ドキュメントは複数のビューを持つことがあります。下図はSOLIDWORKSというCADで同じドキュメントを4つのビューを使って異なる視点で眺めている状態です。
ソースコード中にクラス、メンバー関数、メンバー変数などを対話形式で追加することのできる機能です。MFC アプリケーションにコードウィザードを利用してコードを追加すると適切なコードが追加されるので正しいコードの生成方法を学ぶことができます。クラスウィザードはよく使用されるコードウィザードのひとつです。
C言語では異なる型を1つの型にまとめるための構造体という機能があります。
構文)
struct 構造体型名 {
型名 識別子;
型名 識別子;
…
};
例)
struct CgsVector {
double m_dX;
double m_dY;
double m_dZ;
};
構文)
構造体型名 構造体変数名;
例)
CgsVector v;
構造体のメンバーへアクセスするにはドット演算子(.)を使用します。
構文)
構造体変数名.メンバー
メンバーへのアクセスを使って値を代入したり参照したりすることができます。
例)
CgsVector v;
v.m_dX = v.m_dY = v.m_dZ = 0.0;
但し、構造体がポインタの場合にはアロー演算子(->) を使用します。
構文)
構造体変数名->メンバー
例)
CgsVector v;
CgsVector* pv = &v;
pv->m_dX = pv->m_dY = pv->m_dZ = 0.0;
クラスはユーザーが作成できるユーザー定義型の一つで一般的な物に関するデータ(性質や状態)や関数(機能)を抽象化してまとめたものです。
クラス宣言を行いデータや関数をまとめて宣言します。クラスの変数をデータメンバー、関数をメンバー関数と呼びます。
構文)
class クラス名 {
アクセス指定子:
変数の宣言;
…
関数の宣言;
…
};
例)
class CgsVector {
public:
CgsVector(const double& x = 0.0, const double& y = 0.0, const double& z = 0.0);
~CgsVector();
const double& GetX() const;
private:
double m_dX, m_dY, m_dZ;
};
クラスからオブジェクトが作成されるときに呼び出されるメンバー関数をコンストラクタと呼びます。コンストラクタには戻り値がありません。
コンストラクタでは:(コロン)を使ってメンバー変数の初期化を行う事ができます。
構文)
クラス名::クラス名(引数リスト)
{
…
}
例)
CgsVector::CgsVector(const double& x, const double& y, const double& z) : m_dX(x), m_dY(y), m_dZ(z)
{
}
クラスからオブジェクトが破棄されるときに呼び出されるメンバー関数をデストラクタと呼びます。
構文)
クラス名::~クラス名()
{
…
}
例)
CgsVector::~CgsVector()
{
}
構文)
戻り値 クラス名::メンバー関数名(引数リスト)
{
…
}
:: はスコープ解決演算子と呼ばれ、メンバーがどのクラスに属するのかを指定します。
例)
const double& CgsVector::GetX() const
{
return m_dX;
}
プログラムが複雑化してくるとクラスのメンバー変数をクラスの外から勝手に変更するなどして不具合が起こるような操作をしてしまう事があります。これを防ぐためにメンバーへのアクセス制限を行います。
private
private 指定されたメンバーはクラスの外からアクセスすることができません。一般にデータメンバーは private 指定しておきます。
protected
基本クラスの private メンバーは派生クラス内からもアクセスする事はできません。派生クラスからもアクセスする為には protected というアクセス指定子を使います。
public
public 指定されたメンバーはクラスの外からアクセスすることができます。一般にデータメンバーの変更はアクセサと呼ばれるメンバー関数を介して行います。
構造体の時と同じようにクラスのメンバーへアクセスするにはドット演算子(.)を使用します。
構文)
クラス変数名.メンバー
例)
CgsVector v(0.0, 0.0, 0.0);
double x = v.GetX();
但し、クラスがポインタの場合にはアロー演算子(->) を使用します。
構文)
構造体変数名->メンバー
例)
CgsVector v(0.0, 0.0, 0.0);
CgsVector* pv = &v;
double x = pv->GetX();
テキストファイルには様々なエンコード方式があり、「house.stl」,「wheel3.stl」,「cone.stl」のエンコードはいずれも BOM無しUTF-8 です。そして今回の設定に沿って作られたプログラムでの文字列のエンコードは UTF-16 で動作しています。Windows は様々な言語で動作するように作られているため、1つのエンコードで多言語を表現できるユニコードを使って文字列を表せるようになったのはとても合理的です。しかし、テキストファイルには様々なエンコード方式で作られてしまっているためにファイルを開くときにそのファイルのエンコードを指定して UTF-16 に変換しなくてはいけません。その変換設定を行うのがロケール設定です。
iostream 系のロケール設定は ios_base::imbue を使い下記の様な記述をするのが一般的ですが、何故かうまくいかない PC があったので、サンプルでは昔ながらの _wfopen_s でエンコードを指定する形でファイルを開くスタイルで記述しています。もし、下記の記述で問題なければこちらの方が良いかもしれません。
bool CgsTriangles::ReadText(const std::wstring& path)
{
// 初期化
clear();
// ファイルを開く
std::wifstream fs(path.c_str());
if (!fs.is_open()) {
return false;
}
fs.imbue(std::locale(".UTF-8"));
// ファイルを閉じる
ifs.close();
return !empty();
}
スタンダードテンプレートライブラリは C++ のテンプレートを活用した決まった型に依存しないオブジェクトを格納するコンテナ、コンテナの要素を参照するためのイテレータ、イテレータで指定されたコンテナの要素に対して特定の操作を行うアルゴリズムなどからなる標準ライブラリです。
コンテナはオブジェクトを保持するデータ構造で、配列, リスト, 集合, 連想配列, スタック, キューなど様々な形式でデータの集まりを表現することができます。
vector は可変長配列を実装するコンテナです。vector を使うとメモリの確保/再確保/解放は vector 自身で自動的に行われますし、要素の挿入/削除など可変長配列を扱い易くするための機能の多くが用意されています。例えば下記の様なものです。これらを使いこなすことができればプログラムを簡潔に書くことができるようになるので、積極的に使っていきましょう。
push_back
新たな要素を末尾に追加します。
insert
指定位置に要素を追加します。
operator[]
指定番目の要素を参照します。
size
要素数を取得します。
ここで三角形データ読み込み処理の可変長配列を malloc と reallock を使った形に置き換えると下記の様になります。いくつかの変数が増えたり要素数やメモリサイズの計算などが必要となり、随分とごちゃごちゃとした感じになってしまいます。
また、ここでは記述していませんが三角形データが不要になった際にはメモリの解放(delete pTriangles;)を適切なタイミングで行う必要があります。
【CgsTriangle.cpp】
// 三角形データ読み込み
CgsTriangle tri;
int count = 0; // 配列数
CgsTriangle* pTriangles = nullptr; // 配列の先頭アドレス
const size_t sz = sizeof(CgsTriangle); // 要素1個分のバイト数
while (tri.ReadText(ifs)) {
if (pTriangles) {
CgsTriangle* p = (CgsTriangle*)realloc(pTriangles, sz * (count + 1)); // 要素 count + 1 個分のメモリ再確保
if (!p) {
break; // メモリ再確保に失敗
}
pTriangles = p; // 再確保したアドレスを保持
}
else {
pTriangles = (CgsTriangle*)malloc(sz); // 要素1つ分のメモリ確保
}
pTriangles[count++] = tri; // 配列の最後の要素に読み込んだ三角形データをコピー
}
範囲 for 文は配列やコンテナを簡潔に記述するための for 文の別表現です。下記の3つの記述は全ての三角形要素を描画する処理ですが範囲 for 文を使うと簡潔に記述することができます。
《範囲 for 文を使用する場合》
for (const auto& e : triangles) {
glNormal(e.GetNormal()); // 法線ベクトル設定
glColor(RGB(192, 192, 192)); // 面色設定
for (int i = 0; i < 3; ++i) {
glVertex(e.GetPoint(i)); // 頂点設定
}
}
《添え字を使用する場合》
const size_t count = triangles.size();
for (size_t i = 0; i < count; ++i) {
glNormal(triangles[i].GetNormal()); // 法線ベクトル設定
glColor(RGB(192, 192, 192)); // 面色設定
for (int j = 0; j < 3; ++j) {
glVertex(triangles[i].GetPoint(j)); // 頂点設定
}
}
《イテレータを使用する場合》
std::vector<CgsTriangle>::const_iterator it = triangles.cbegin();
for (; it != triangles.cend(); ++it) {
glNormal(it->GetNormal()); // 法線ベクトル設定
glColor(RGB(192, 192, 192)); // 面色設定
for (int i = 0; i < 3; ++i) {
glVertex(it->GetPoint(i)); // 頂点設定
}
}
サンプルでは使用しませんでしたが CgsVector や CgsPoint に演算子のオーバーロードを記述することで加算や減算などの処理を直感的に記述することができるようになります。もし、サンプルの機能拡張を続けていくのであればこれらはとても役に立つでしょう。
ベクトル値を使って計算できそうな演算子をオーバーロードしてみます。
【CgsVector.h】
class CgsVector
{
public:
// 省略
inline CgsVector operator+(const CgsVector& another) const // 加算
{
return { m_dX + another.GetX() , m_dY + another.GetY(), m_dZ + another.GetZ()};
}
inline CgsVector operator-(const CgsVector& another) const // 減算
{
return { m_dX - another.GetX() , m_dY - another.GetY(), m_dZ - another.GetZ() };
}
inline CgsVector operator*(const double& d) const { return { m_dX * d, m_dY * d, m_dZ * d }; } // 乗算
inline CgsVector operator/(const double& d) const { return { m_dX / d, m_dY / d, m_dZ / d }; } // 除算
inline CgsVector& operator+=(const CgsVector& another) { return *this = *this + another; } // 加算代入
inline CgsVector& operator-=(const CgsVector& another) { return *this = *this - another; } // 減算代入
inline CgsVector& operator*=(const double& d) { return *this = *this * d; } // 乗算代入
inline CgsVector& operator/=(const double& d) { return *this = *this / d; } // 除算代入
// 省略
};
inline CgsVector operator*(const double& d, const CgsVector& v) { return v * d; } // 乗算
座標値を使って計算できそうな演算子をオーバーロードしてみます。
【CgsPoint.h】
#pragma once
#include <string>
#include "CgsVector.h"
class CgsPoint
{
// 省略
inline CgsPoint operator+(const CgsVector& v) const // ベクトル加算
{
return { m_dX + v.GetX(), m_dY + v.GetY(), m_dZ + v.GetZ() };
}
inline CgsVector operator-(const CgsPoint& another) const // 座標値減算
{
return { another.GetX() - m_dX, another.GetY() - m_dY, another.GetZ() - m_dZ };
}
// 省略
};
ついでにベクトル演算では下記の処理を行うことがあるので実装してみます。
長さ
単位ベクトル
内積
外積
【CgsVector.h】
class CgsVector
{
public:
// 省略
inline double GetNorm() const { return sqrt(m_dX * m_dX + m_dY * m_dY + m_dZ * m_dZ); } // 長さ
inline CgsVector& Normalize() { return (*this /= GetNorm()); } // 単位ベクトル
inline double Dot(const CgsVector& another) const // 内積
{
return m_dX * another.m_dX + m_dY * another.m_dY + m_dZ * another.m_dZ;
}
inline CgsVector Cross(const CgsVector& another) const // 外積
{
return {
m_dY * another.m_dZ - m_dZ * another.GetY(),
m_dZ * another.m_dX - m_dX * another.GetZ(),
m_dX * another.m_dY - m_dY * another.GetX()
};
}
// 省略
};
例えば下記の様に座標値やベクトル値の計算に演算子を用いた形で記述できるようになります。
void Test()
{
CgsPoint p1(1.0, 2.0, 3.0);
const CgsPoint p2 = p1 + CgsVector(4.0, 5.0, 6.0);
CgsVector v1 = p2 - p1;
CgsVector v2 = v1 + CgsVector(7.0, 8.0, 9.0);
const CgsVector v3 = v1 - v2;
const CgsVector v4 = v1 * 2.0;
const CgsVector v5 = v1 / 2.0;
const CgsVector v6 = v1.Normalize();
const CgsVector v7 = v1.Cross(v2);
const double d1 = v1.Dot(v2);
const double d2 = v1.GetNormal();
v1 += v2;
v2 -= v7;
v1 *= d1;
v2 /= d2;
p1 = p1 + v6;
}
OpenGL では glVertex 系関数を使って頂点を指定することで図形を描画しますが、描画される図形は glBegin と glEnd で区切られるグループのタイプによって異なります。glBegin の引数に指定できる図形のタイプ(図形プリミティブ)には以下のようなものがあります。
GL_POINTS
点を打つ。
GL_LINES
2点を対にして、その間を直線で結ぶ。
GL_LINE_STRIP
折れ線を描く。
GL_LINE_LOOP
折れ線を描く。始点と終点の間も結ばれる。
GL_TRIANGLES
3点を組にして、三角形を描く。
GL_QUADS
4点を組にして、四角形を描く。
GL_TRIANGLE_STRIP
1辺を共有しながら帯状に三角形を描く。
GL_QUAD_STRIP
1辺を共有しながら四角形を描く。
GL_TRIANGLE_FAN
1辺を共有しながら扇状に三角形を描く。
GL_POLYGON
凸多角形を描く。
プログラムの作成過程で今の状態をバックアップしておきたいということがあると思います。基本的にはソリューションを作成した「STLViewer」フォルダをコピーするか圧縮してファイル名を変更しておけば問題ありません。
下記のフォルダにできるファイルはファイルサイズがとても大きくなることがあります。ファイル自体は Visual Studio でソリューションを読み込んだり、ビルドを行った時に自動的に生成されるのでバックアップ時には削除して下さい。
「.vs」フォルダ
Visual Studio でソリューションを操作した時の情報などのファイルを保持するフォルダ
「Debug」フォルダ
x86 プラットフォームの Debug 版をビルドした時に生成されるフォルダ
「Release」フォルダ
x86 プラットフォームの Release 版をビルドした時に生成されるフォルダ
「x64」フォルダ
x64 プラットフォームをビルドした時に生成されるフォルダ