共通関数群を作ろう1
今回は、共通関数群を作ると題して、カラーダイアログ、フォルダ選択ダイアログ、そして絶対パスから相対パスを作りだすテクニックについて解説していきます。特に絶対パスから相対パスを作り出すテクニックは、次章で使いますのでここは必ず習得してくださいね。この章はかなり長いので頑張っていきましょうね!
14.共通関数群とは
共通関数群となんだか堅苦しい名前ですが、例えばCMyHtmlView.cppで使うローカルな関数(CMyHtmlViewクラスのメンバ関数ではない関数のこと)は、CMyHtmlView.cppに記述しなければなりません。しかし、その関数を他の.cppファイル内でも使いたい…となると、また同じコードで関数名を変えた書いたりと…とにかく面倒なことが生じます。そこでプロジェクト内に、これからの共通に使える関数を、別の.hと.cppファイルに記述しておき(例えばmyhtmlcom.hやmyhtmlcom.cpp)、それらの関数を使いたいファイルでmyhtmlcom.hをインクルードすれば、バグが少ないコードが記述できるという仕掛けです。

したがって、この共通関数群ファイルに記述されるべき関数は、インクルードさえしてしまえばどのクラスからも参照できるために、いちいち書くのが面倒くさいコードや、よく使う関数群をまとめておくと、非常に便利になります。とりあえず、プロジェクトにファイルを追加する手順を説明していきます。
1.ヘッダーファイル(*.h)の追加(新規作成)
[ファイル]-[新規作成]でmyhtmlcom.hを新規に作成する。このときプロジェクトに追加がチェックされていることに注意する。
2.ソースファイル(*.cpp)の追加(新規作成)
[ファイル]-[新規作成]でmyhtmlcom.cppを新規に作成する。このときプロジェクトに追加がチェックされていることに注意する。
3.FileViewをクリック
さきほど新規に作成した二つのファイルが追加されていれば完了
ところでmyhtmlcomの意味ですが、MyHtmlプロジェクトのコモン(common)ファイルという意味で、myhtmlcomとしておきました。このファイル名は各自ご自由ですが、インクルードするときには、各自設定した名前でインクルードするように注意してください。ここではmyhtmlcomで説明していきます。

では、myhtmlcom.hを開いて(真っ白なはずですが)ください。以下に示すコードを書いてください。(今回共通関数群として3つの関数と、2つのマクロを定義しておきます。)
//////////////////////////////////////////////////
// myhtmlcom.h MyHtml共通関数
//////////////////////////////////////////////////

#include "stdafx.h"

//マクロ
#define GetMyDocument() (CMyHtmlDoc*)(((CFrameWnd*)(AfxGetApp()->m_pMainWnd))->GetActiveDocument());
#define GetMyView() (CMyHtmlView *)(((CFrameWnd*)(AfxGetApp()->m_pMainWnd))->GetActiveView());

// 関数
bool SelectFolder(CString&); // フォルダ選択ダイアログ゛
CString GetRelativePath(CString,CString); // 相対パスを取得
CString GetColorString();// カラーダイアログ
二つのマクロは、DocumentとViewのポインタを取得するマクロです。Tipsにも載せてあるコードをマクロ化しただけですが、結構便利なのでこのまま記述しておいてください。

簡単ですが、解説をしていきます。
#include "stdafx.h"
MFCを使うのでインクルードしておきます。
bool SelectFolder(CString&);
フォルダ選択ダイアログを開き、ディレクトリ(フォルダ)の絶対パスを取得する関数です。この関数はすぐには使用しませんが、11週目以降のおまけ講座として使う可能性があります。
CString GetRelativePath(CString,CString);
二つの絶対パスから、2つの間の相対パスを生成します。これは、コンピュータ内のファイルのリンクなどに使われます。
CString GetColorString();
カラーダイアログを開き、RGBカラーコードを取得します。
それでは1つ1つ作っていくことにします。
15.SelectFolder関数のコーディングをする
みなさんも左のような画面のダイアログをどこかで1回くらいは見たことがあると思います。例えばインストール時にどのフォルダにインストールするのか?なんて時にもでてきたりしますし、私が使っている解凍ソフト(かなり有名なソフト)にもでてきたりします。このフォルダ選択ダイアログは、Windowsユーザなら全く同じ者を見ることになります。特にこれらをコモンダイアログといい、Windowsで共通に使用されているダイアログのことを指します。MFCでは以下のコモンダイアログが用意されています。
CColorDialog
色の選択ダイアログ
CFileDialog
開く・保存するファイル名の選択
CFindReplaceDialog
ファイルやテキストにおける検索・置換の操作
CFontDialog
フォント(フォント名・属性・サイズなど)の指定
CPrintDialog
印刷処理用の情報の指定を行う
あれ?と思った方は鋭い人です。フォルダ選択ダイアログがないんですね。つまりフォルダ選択ダイアログはMFCクラスとして用意されていないのです。フォルダ選択ダイアログはAPI(Application Programing Interface)というC言語で記述されたインターフェースで定義されています。MFCに対してSDKがあることは以前紹介したかもしれませんが、SDKはAPIを多く使ってコードを記述します。SDKプログラマーならともかくMFCプログラマーは、MFCでサポートされていない部分はAPIを使用して補うしかないのです。

まずフォルダ選択ダイアログを使用するのに以下のコードを準備として書き込んでいきます。
#include "stdafx.h"
#include "myhtmlcom.h"
#include <shlobj.h>
#include <direct.h>
#include <stdlib.h>
#include <stdio.h>
int __stdcall BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData){
if (uMsg == BFFM_INITIALIZED){
SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
}
return 0;
}
なんだか見慣れない関数が出てきました…。BrowseCallbackProc(コールバックプロシージャ)というものですが、これはこれでこういうものだと思ってコードを記述したほうがいいかもしれません。MSDNの説明も結構適当で、
BrowseCallbackProc関数は、SHBrowseForFolder関数で使用される、アプリで定められたコールバック関数を指定し、フォルダ選択ダイアログは、それにイベントについて知らせるためにこの関数を呼ぶ
なんてよく分からないことを書いています。かみくだいて説明すれば、
SHBrowseForFolder関数を使ってダイアログを表示したとします。そこで何らかのイベントを起こしたとします。例えば、フォルダを選択するとかです。するとこの関数が呼ばれてなにかを実行し、またSHBrowseForFolder関数に帰ってくる。 ものだと思っても結構です。

とは言うものの、実際おこるイベントは、「ダイアログが表示される」か「選択されるフォルダが変更される」かの2つがメインですが、別に選択されるフォルダが変更される度に実行する関数は、今回必要としないので、ダイアログが表示される瞬間に実行されるコードだけを記述したのが上のコードです。初めに選択しておくフォルダを開いておくコードです。これはなくてもいいのですが、あったほうが便利でしょう。 それでは本題のSelectFolder関数を記述していきます。さっきのコードの下に続けて書いてください。
bool SelectFolder(CString& strDir){
BROWSEINFO bInfo;
LPITEMIDLIST pIDList;
TCHAR szDisplayName[MAX_PATH];
char buffer[_MAX_DIR];
// BROWSEINFO構造体に値を設定
bInfo.hwndOwner = AfxGetMainWnd()->m_hWnd; // ダイアログの親ウインドウのハンドル
bInfo.pidlRoot = NULL; // ルートフォルダをデスクトップフォルダとする
bInfo.pszDisplayName = szDisplayName; // フォルダ名を受け取るバッファへのポインタ
bInfo.lpszTitle = _T("フォルダの選択"); // ツリービューの上部に表示される文字列
bInfo.ulFlags = BIF_RETURNONLYFSDIRS; // 表示されるフォルダの種類を示すフラグ
bInfo.lpfn = BrowseCallbackProc; // BrowseCallbackProc関数のポインタ
// 現在のカレントディレクトリの取得
if(_getcwd(buffer,_MAX_DIR )==NULL)
bInfo.lParam = NULL;
else
bInfo.lParam = (LPARAM)buffer;
// フォルダ選択ダイアログを表示
pIDList = ::SHBrowseForFolder(&bInfo);
// ダイアログが閉じた後
if(pIDList == NULL){return false;} //何も選択されなかった場合
else{
// ItemIDListをパスに変換(パスはTCHAR型のszDisplayNameに格納)
if(!::SHGetPathFromIDList(pIDList, szDisplayName)) return false;//変換に失敗
::CoTaskMemFree( pIDList );// pIDListのメモリを開放
strDir = (CString)szDisplayName;//CString型に変更
return true;
}
}
※このSelectFolder関数は、選択に成功した時にtrue,失敗した時にfalseを返すように設計されています。
簡単に解説していきます。
BROWSEINFO bInfo;
LPITEMIDLIST pIDList;
二つの変数宣言ですが、前者はBROWSEINFO構造体といい、フォルダ選択ダイアログの設定を行う構造体で、後者はフォルダ選択ダイアログからの戻り値となります。
bInfo.hwndOwner = AfxGetMainWnd()->m_hWnd;
ウィンドウのハンドルが入ります。
bInfo.pidlRoot = NULL;
ルート、すなわちフォルダ選択ダイアログのトップのフォルダを指定することができます。NULLの場合はデスクトップになるます。
bInfo.pszDisplayName = szDisplayName;
選択されたフォルダ名を受け取るバッファへのポインタです。pIDListからフォルダ名を受け取る時に使用します。
bInfo.lpszTitle = _T("フォルダの選択");
ダイアログの上部に書かれる文字列です。ダイアログのタイトルではありません。タイトルを変更する場合には、BrowseCallbackProc関数のSendMessage関数の直前あたりに::SetWindowText(hWnd,_T("***"));と書けばいいと思います。(動作確認はしていませんが(^^;))
bInfo.ulFlags = BIF_RETURNONLYFSDIRS;
フォルダ選択ダイアログの仕組みを決めます。BIF_RETURNONLYFSDIRSは、フォルダ(ディレクトリ)情報だけをpIDListに返し、適切なフォルダが選択されるまではOKボタンが淡色表示されるという特徴があります。一番一般的です。
bInfo.lpfn = BrowseCallbackProc;
待ってました!先ほど作ったBrowseCallbackProc関数へのポインタを格納します。ここをNULLで指定してもいいのですが、初期選択するディレクトリがオープンされなくなってしまうので、せっかくBrowseCallbackProcを書いたのだったら指定しておいたほうが便利です。
if(_getcwd(buffer,_MAX_DIR )==NULL)
bInfo.lParam = NULL;
else
bInfo.lParam = (LPARAM)buffer;
_getcwdでカレントディレクトリを調べ、パスをbufferに書き込みます。bufferがNULL(カレントディレクトリが判別できない)でないときは、bInfo.lParamに書き込みます。これが最初に開いておくディレクトリ名になります。ちなみにBrowseCallbackProc関数の引数lpDataは、このbInfo.lParamのことであることからも、最初に選択され開かれるディレクトリになることがわかるでしょう。
pIDList = ::SHBrowseForFolder(&bInfo);
フォルダ選択ダイアログを開きます。その結果をpIDListに格納します。
if(pIDList == NULL){return false;}
pIDListがNULLであったらフォルダが選択されていないのでfalse(失敗)を返し終了します。
else{
if(!::SHGetPathFromIDList(pIDList, szDisplayName)){return false;}
pIDListからディレクトリ名を生成します。ディレクトリ名はszDisplayNameに書き込まれますが、生成に失敗したらfalseを返し終了します。
::CoTaskMemFree(pIDList);
メモリをクリア(開放)します。
strDir = (CString)szDisplayName;
TCHAR型から使いやすいCString型に変更します。
return true;
成功の意味で、trueを返し終了します。
}
この関数を使う側でtrue,falseを捕まえれば、ディレクトリ名を取得できたかどうかを判断できるはずです。では早速使用したい…のですが、まだMyHtml内部で使用するチャンスはないので、簡単なテストコードを書いておきます。これで実行できたかできないかを調べてみてください。(ViewなりDocなり適当なところでこのコードを書いて呼んであげればいいわけですが)
#define "myhtmlcom.h" ←これをかかないと×

(中略)
CString strPath;
if(SelectFolder(strPath))
AfxMessageBox(strPath);
else
AfxMessageBox("Not Found.");
で、動作確認できると思います。今回作成したフォルダ選択ダイアログは、いろんな場所で使用できますから、是非習得しおくと、ソフトのバリエーションが広がるのではないでしょうか?
16.GetRelativePathのコーディングをする
まずは、絶対パスと相対パスの説明から入りたいと思います。絶対パスとは例えば
C:\Program Files\Microsoft Visual Studio\MyProjects\ConsoleApp\test.cpp
などのように1つのファイル・ディレクトリについて、その上の階層のディレクトリを全て記述したものを指します。絶対パスが分かれば、ウィンドウズの「ファイル名を指定して実行」でこの絶対パスを入力するだけでファイルを開くことができます。絶対パスは、そのファイルの位置情報といってもいいでしょう。
HTMLでは絶対パスと相対パスの区別は非常に重要です。例えば他のwwwサイトをリンクする場合には、絶対パスによるリンクでなくてはなりません。しかし自分のPCのファイルでホームページを作るときなどは、絶対パスでリンクしてしまうと、不都合が生じます。最終的にはファイルをプロバイダなどのサーバーにアップするのですが、絶対パスリンクだと、絶対パスのファイルを読み込んでしまいうまくリンクできないのです。また、あるフォルダ内で自分のPC内でうまく絶対パスリンクされいるとします。そのフォルダ別のディレクトリに移すだけで全部リンクが不正になってしまうのです。そこで相対パスというものが重要になります。
では相対パスについて右の図で説明していきます。"相対"なわけで、二つのパスのどちらかが基準になり、その基準からの位置関係を示したものが相対パスです。
1つ上のディレクトリを指す時には、".."を指定し、ディレクトリの区切りは"/"で行います。二つ上のディレクトリは"../../"となるわけですが、詳しくは以下の数種類の例で相対パスがどんなものかわかると思います。
基準位置view.cppに対するmake.cppの相対パス 
make.cppは、view.cppのディレクトリMFCの1個上のディレクトリ階層のファイルなので、
../make.cpp
基準位置class.cppに対するclass.hの相対パス
同じディレクトリ内部なのでファイル名そのものが相対パスとなり
class.h
基準位置class.cppに対するview.cppの相対パス
まずC++の一つ上にあがってあとは下がっていけばいいので
../visual/MFC/view.cpp
少しずつ分かっていただけたでしょうか?では問題です。基準位置view.cppに対するclass.cppの相対パスは?
"../../C++/class.cpp"ココを探してくださいね

で本題ですが、これをプログラムで行うにはどうしたらよいかということになります。そんな便利な関数がありそうですが、ないので(^^;)自作するほかありませんね。それがGetRelativePath関数です。まずはコードを記述する前に、簡単な概念図で、相対パスの作り方を考えていきます。

①二つの絶対パスを用意する
上の絶対パスが基準パスで、下の絶対パスが相対パスを求めたいパスになります。
②基準パスのファイル名を削除する
基準になるのはこのファイルがあるディレクトリなのでココはなくてもいいのです。
③二つの絶対パスの先頭からの共通部分を削除
絶対パスの絶対パスの先頭からの共通部分してしまいます。ここは相対パスには不要だからです。
④残った基準パスのディレクトリ数をカウントする
これがディレクトリバック"../"の個数になります
⑤ディレクトリバック文字列を生成する
④で求めた数の"../"文字列を作成します。
⑥相対パスを作成する
④で残った(相対パスを求めたいほうの)パスを上で作成した文字列に加えればよいです。
これをコード化したものが下です。やっていることは上で説明した①〜⑥なので全て細かく説明するのは割愛させていただきますが、あまり使わない関数について説明しておきます。
// strA 基準パス
// strB 相対パスを計算させたいパス
CString GetRelativePath(CString strA,CString strB){
CString strBasePath = strA;
CString strPath = str B;
CString strBackDir; // ../文字列
CString strDir;// 相対パス
int nBackDir = 0;// ../を記述する回数
int i,nNumHtml,nNumPath;// Loop変数
char chDrv[_MAX_DRIVE];// Htmlのドライブ
char chDir[_MAX_DIR];// Htmlのディレクトリ
CString strTemp;// CStringテンポラリファイル
bool bFlg = true;// フラグ
// strBasePathからファイル名だけを取り除く②
_splitpath(strBasePath,chDrv,chDir,NULL,NULL);
strBasePath = (CString)chDrv + (CString)chDir;
// 先頭から共通なデ゛ィレクトリを削除③
while(bFlg){
if(strHtmlPath.SpanExcluding("\\")==strPath.SpanExcluding("\\")){
strTemp = strHtmlPath.SpanExcluding("\\");
strHtmlPath.Delete(0,strTemp.GetLength()+1);
strPath.Delete(0,strTemp.GetLength()+1);
}
else bFlg = false;
}
// ../と記述する数nBackDirをカウント④
while(!strBasePath.IsEmpty()){
strBasePath.Delete(0,strBasePath.SpanExcluding("\\").GetLength()+1);
nBackDir++;
}
// ../文字列作成⑤
for(i=0;i<nBackDir;i++)
strBackDir += "../";
//相対パス作成⑥
strDir = strBackDir + strPath;
// '\'と'/'の変更
for(i=0;i<strDir.GetLength();i++){
if(strDir.GetAt(i)=='\\') strDir.SetAt(i,'/');
}
// 先頭が'/'の場合は削除
if(strDir.GetAt(0)=='/') strDir.Delete(0,1);
return strDir;
}
※このコードにはバグが残っている可能性があります。
_splitpath(const char *pchPath,char *pchDrive,char *pchDir,char *pchFname,char *pchExt)
パスpchPathを構成要素(ドライブ、ディレクトリパス、ファイル名、拡張子)に分解します。今回のようにファイル名と拡張子の構成要素を求める必要がない場合のように、パスを求めなくてもいい要素にはNULLを指定します。<stdlib.h>を必要とします。
CString::SpanExcluding(CString strCharSet)
strCharSetで指定した文字セット内の文字が最初に出現するまで文字列を検索し、strCharSetと一致する文字が見つかるまでの文字をすべて抽出します。
CString str = "abcdefghijk";
CString str2 = str.SpanExcluding("e");
とするとstr2は、"abcd"となります。詳しくはMSDNを見てください。この関数は意外によく使用します。
CString::Delete(int nStart,int nCount)
文字列のインデックスがnStartからnCount個の文字を削除します。
CString str = "abcdefghijk";
str.Delete(1,2);
をするとstrは"adefghijk"となります。これも詳しくはMSDNを見てください。また文字列を空にするにはCString::Empty()なので間違わないようにしましょう。Deleteでも文字列をクリアすることができますが、面倒くさいでしょう。
コードの最後のほうで「'\'と'/'の変更」なんてことを行っていますが、環境によって、ディレクトリを区切る記号が"\"だったり"/"だったりするので全て"/"で統一するために行っています。

さてこの関数の確認ですが、とりあえずコンパイルエラーがないようにしてもらえればいいです。実行結果が正しいか正しくないかは次章以降で検討していきましょう。

[Next]
[Previous]
[Home]