2016年5月3日火曜日

MFC オートコンプリートというより選択リストから選択できるように。

エディットコントロールにコンボボックスのようにリストボックスをつけて、エディットの入力内容に基づいて選択候補を表示するというものが欲しかった。コンボボックスではどうしても操作がおかしくなるので実現できなかった。IAutoCompleteは訳がわからない。フィルターを切っても、内部で訳の解らないフィルターをされるので、自由に表示できず、全く使えなかった。
単純にこちらで指定した文字列群を表示して選択ができるだけのインタフェースが見つからなかった。
で、作成してみた


クラス名:AcEdit   これ自体にリストボックスを持たせてある。
AcEdit.h
-------------------------------
#pragma once
#include "afxcoll.h"
#include "PopupListBox.h"


// AcEdit

class AcEdit : public CEdit
{
 DECLARE_DYNAMIC(AcEdit)

public:
 AcEdit();
 virtual ~AcEdit();

protected:
 DECLARE_MESSAGE_MAP()
public:
private:
 // リストボックスITEM
 CStringArray m_straryListItem;
public:
 // ボップアップ
 CPopupListBox *m_pPopListBox; // これがポップアップリストボックスダイアログ
 void ResetContent();
 void AddString(LPCTSTR lpszString);
 void ShowDropDown(bool bShow);

 afx_msg LRESULT OnUserSelChange(WPARAM wParam, LPARAM lParam);
 afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
 void Moved();
};
-----------------------------------------

AcEdit.cpp
-----------------------------------------
// AcEdit.cpp : 実装ファイル
//

#include "stdafx.h"
#include "APPLICATION.h"  ダイアログのため。
#include "AcEdit.h"


// AcEdit

IMPLEMENT_DYNAMIC(AcEdit, CEdit)

AcEdit::AcEdit()
{
 this->m_straryListItem.SetSize(0, 1500);
 this->m_pPopListBox = new CPopupListBox();
}

AcEdit::~AcEdit()
{
 delete m_pPopListBox;
 m_pPopListBox = NULL;
}


BEGIN_MESSAGE_MAP(AcEdit, CEdit)
 ON_MESSAGE(WM_USER_SEL_CHANGE, OnUserSelChange)
 ON_WM_KEYDOWN()
END_MESSAGE_MAP()



// AcEdit メッセージ ハンドラー

void AcEdit::ResetContent()
{
 if (!::IsWindow(m_pPopListBox->m_hWnd))
 {
  m_pPopListBox->Create(this);
 }
 m_pPopListBox->RemoveString();
 ShowDropDown(false);
}


void AcEdit::AddString(LPCTSTR lpszString)
{
 if (!::IsWindow(m_pPopListBox->m_hWnd))
 {
  m_pPopListBox->Create(this);
 }
 m_pPopListBox->AddString(lpszString);

}


void AcEdit::ShowDropDown(bool bShow)
{
 if ((m_pPopListBox) && (::IsWindow(m_pPopListBox->m_hWnd)))
 {
  if (bShow)
  {
   m_pPopListBox->ShowWindow(SW_SHOWNA);
  }
  else
  {
   m_pPopListBox->ShowWindow(SW_HIDE);
  }
 }
}
LRESULT AcEdit::OnUserSelChange(WPARAM wParam, LPARAM lParam)
{
 /// list選択した通知
 CString strA;
 m_pPopListBox->GetCurSelString(strA);
 this->SetWindowTextA(strA);
 this->SetFocus();
 return 0L;
}


void AcEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
 if ((nChar == VK_UP) || (nChar == VK_DOWN) || (nChar == VK_TAB))
 {
  if (m_pPopListBox)
  {
   m_pPopListBox->StartSel();
  }
 }

 CEdit::OnKeyDown(nChar, nRepCnt, nFlags);
}


void AcEdit::Moved()
{
 if (m_pPopListBox)
  m_pPopListBox->Reshow();
}
-----------------------------------------------------
popuplistbox.h
-----------------------------------------------------
#pragma once
#include "afxwin.h"

// CPopupListBox ダイアログ
#define WM_USER_SEL_CHANGE  (WM_USER + 0x601)

class CPopupListBox : public CDialogEx
{
 DECLARE_DYNAMIC(CPopupListBox)

public:
 CPopupListBox(CWnd* pParent = NULL);   // 標準コンストラクター
 virtual ~CPopupListBox();

 // ダイアログ データ
 enum { IDD = IDD_LISTBOX };

protected:
 virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV サポート

 DECLARE_MESSAGE_MAP()
public:
 afx_msg void OnNcDestroy();
 virtual void OnOK();
 virtual void OnCancel();
 // データ表示用
 CListBox m_listBox;
 afx_msg void OnLbnSelchangeListAc();
 int AddString(LPCTSTR lpszString);
 // 登録文字列を削除
 void RemoveString();
 // 選択中の文字を取得する。
 int GetCurSelString(CString & strSelString);
 void Create(CWnd *pWnd);
 CWnd *m_pWnd;
 void StartSel();
 void Reshow();

 afx_msg void OnLbnDblclkListAc();
};
----------------------------------------------
popuplistbox.cpp
----------------------------------------------
// PopupListBox.cpp : 実装ファイル
//

#include "stdafx.h"
#include "APPLICATION.h" // リソースのため
#include "PopupListBox.h"
#include "afxdialogex.h"

// CPopupListBox ダイアログ

IMPLEMENT_DYNAMIC(CPopupListBox, CDialogEx)

CPopupListBox::CPopupListBox(CWnd* pParent /*=NULL*/)
 : CDialogEx(CPopupListBox::IDD, pParent)
{
 m_pWnd = NULL;

}

CPopupListBox::~CPopupListBox()
{
}

void CPopupListBox::DoDataExchange(CDataExchange* pDX)
{
 CDialogEx::DoDataExchange(pDX);
 DDX_Control(pDX, IDC_LIST_AC, m_listBox);
}


BEGIN_MESSAGE_MAP(CPopupListBox, CDialogEx)
 ON_WM_NCDESTROY()
 ON_LBN_SELCHANGE(IDC_LIST_AC, &CPopupListBox::OnLbnSelchangeListAc)
 ON_LBN_DBLCLK(IDC_LIST_AC, &CPopupListBox::OnLbnDblclkListAc)
END_MESSAGE_MAP()


// CPopupListBox メッセージ ハンドラー




void CPopupListBox::OnOK()
{
 // TODO: ここに特定なコードを追加するか、もしくは基底クラスを呼び出してください。

 // CDialogEx::OnOK();
 if (m_pWnd)
  m_pWnd->PostMessageA(WM_USER_SEL_CHANGE);
}


void CPopupListBox::OnCancel()
{
 // TODO: ここに特定なコードを追加するか、もしくは基底クラスを呼び出してください。

 // CDialogEx::OnCancel();
}



int CPopupListBox::AddString(LPCTSTR lpszString)
{
 if (IsWindowEnabled())
 {
  int ret = m_listBox.AddString(lpszString);
  CRect rect1;
  m_listBox.GetWindowRect(rect1);
  int nW = rect1.Width();
  CDC *pDC = m_listBox.GetDC();
  CSize size1 = pDC->GetTextExtent(lpszString);
  ReleaseDC(pDC);
  if (size1.cx > nW)
   nW = size1.cx; // 最大の値とする。

  int nH = 0;
  if (m_listBox.GetCount() > 20)
  {

   nH = m_listBox.GetItemHeight(0) * 20 + 6;
   nW += 24; // ScrollBar
  }
  else
  {
   nH = m_listBox.GetItemHeight(0) * m_listBox.GetCount() + 6;
  }
  rect1.bottom = rect1.top + nH;
  rect1.right = rect1.left + nW;
  this->MoveWindow(rect1);
  ScreenToClient(rect1);
  m_listBox.MoveWindow(rect1);
  return ret;
 }
 return -1;
}


// 登録文字列を削除
void CPopupListBox::RemoveString()
{
 if (IsWindowEnabled())
 {
  CRect rect1;
  m_listBox.GetWindowRect(rect1);
  ScreenToClient(rect1);
  rect1.right = rect1.left + 60;
  m_listBox.MoveWindow(rect1);
  this->m_listBox.ResetContent();
 }
}


// 選択中の文字を取得する。
int CPopupListBox::GetCurSelString(CString & strSelString)
{
 int nIdx = 0;
 if (IsWindowEnabled())
 {
  nIdx = m_listBox.GetCurSel();
  if (nIdx >= 0)
  {
   m_listBox.GetText(nIdx, strSelString);
  }
 }
 return nIdx;
}


void CPopupListBox::Create(CWnd *pWnd)
{
 CDialog::Create(IDD, pWnd);
 m_pWnd = pWnd; // 呼び出し元
 Reshow();
 m_pWnd->SetFocus();
}
void CPopupListBox::Reshow()
{
 CRect rect1;
 if (m_pWnd && ::IsWindow(m_pWnd->m_hWnd))
 {
  m_pWnd->GetWindowRect(rect1);
  this->SetWindowPos(NULL, rect1.left, rect1.bottom, 0, 0, SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE);
 }
}


void CPopupListBox::StartSel()
{
 m_listBox.SetFocus();
 this->m_listBox.SetCurSel(0);
}


void CPopupListBox::OnLbnDblclkListAc()
{
 if (m_pWnd)
  m_pWnd->PostMessageA(WM_USER_SEL_CHANGE);
}

----------------------------------------------------
Dialog定義
----------------------------------------------------
IDD_LISTBOX DIALOGEX 0, 0, 139, 9
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    LISTBOX         IDC_LIST_AC,0,0,138,9,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
END


MFC経験者ならそれなりに理解できると思います。リストボックスの横幅の処理がまずいですが、なんとかなるでしょ。 勝手に修正してください。 ダイアログ以外でなんとかしようと思ったのですが、、、使えるものが見当たらないので、自作です。 上位のダイアログエディタで、EditBoxを配置した後、変数追加でAcEditクラスの変数 m_edtFind として追加します。 AcEditの通知関数は、EN_CHANGEを追加して、下記のように。

.....
 ON_EN_CHANGE(IDC_EDIT_FIND_R, &CDFindNext::OnEnChangeEditFindR)
.....
void CDFindNext::OnEnChangeEditFindR()
{
 CString strA;
 this->m_edtFind.GetWindowTextA(strA);
 if (strA.GetLength())
 {
  char cC = strA.GetAt(0);
  if ((cC >= '0') && (cC <= '9'))
   this->m_btnOK.SetWindowTextA("Go");
  else
   this->m_btnOK.SetWindowTextA("検索");
  m_bFirst = true;
  SetStrings();
  SetTimer(TM_IDX_DROPDOWN, 500, NULL);
 }
 else
 {
  m_edtFind.ShowDropDown(false); // 消す
 }
}
void CDFindNext::SetStrings(void)
{
 CStringArray straryF;
 straryF.SetSize(0, 1500);
 CString strA;
 m_edtFind.GetWindowTextA(strA);
 if (strA.GetLength())
 {
  if (((CMainFrame *)AfxGetMainWnd())->FindComments(strA, straryF))
  {

   m_edtFind.ResetContent(); ///リストボックス文字列を消します。
   for (int i = 0; i < straryF.GetCount(); i++)
   {
    CString strAA = straryF.GetAt(i);
    strAA.Trim();
    m_edtFind.AddString(strAA);/// 文字列セット
   }
   m_edtFind.ShowDropDown(true); //// 表示指令
  }
 }

}


ここまで書けばなんとかなるでしょ。 作ったばかりですが、大きな問題はなさそうですのでUPしました。 細かいところは自分で修正してください。 余計な定義が残っているかもしれません。よろしく対処してください。
FindCommnetsは自作の選択項目の検索モジュールです。ここのstraryFがリストに表示されるという塩梅です。