☆金額をひたすら入力する時・・・。

仕事で数十万円から数千万円の金額をひたすら入力しなければならなかったので、 下記のような処理で、テンキーの[ . ](コンマ)を[000](ゼロ3つ)に使えるようにしました。
[000]キーがあるUSBテンキーが手元になかったので、とても便利でした。
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  if Key = '.' then
  begin
    Edit1.Text := Edit1.Text + '000';
    Key := #0;
    Edit1.SelStart := Length(Edit1.Text);
  end;
end;

|

☆BDE Table.Locateで・・・。

ずいぶん昔に作ったBDEを使ったプログラムをDelphi2007で再コンパイルして仕事をしていたところ、 「この操作は行えません」というエラーメッセージが頻繁に表示されるようになりました。

そこでシンプルなサンプルを作って、原因を探したところ、Locateメソッドの検索文字がフィールドサイズより長い場合にエラーになっていました。(そんな仕様だっけ?)

又、この問題とは別に、TEditのMaxLengthがランタイムテーマの影響を受けることが確認できました。 検索文字入力用Editには、元々MaxLengthをフィールド長さで設定してあり、今まで検索文字がフィールドサイズを超えることがなかったのですが、ランタイムテーマの影響で、バイト数から文字数扱いになってしまい、エラーが起こったようです。

今回は、文字長さをバイト数で計算して、フィールド長さ以下であることを確認の上、Locateメソッドに渡すことにより、この問題を解決しました。

※テスト用プログラムです。データベースは、DBDEMOSのemployee.dbを使っています。
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Grids, DBGrids, DB, DBTables;

type
  TForm1 = class(TForm)
    Table1: TTable;
    DataSource1: TDataSource;
    DBGrid1: TDBGrid;
    Button1: TButton;
    Edit1: TEdit;
    Edit2: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  S1, S2: String;
begin
  S1 := Edit1.Text;
  S2 := Edit2.Text;
  Table1.Locate('LastName;FirstName',VarArrayOf([S1, S2]), [loPartialKey]);
end;

end.

Locate

|

☆TEditにインデント機能をつける。

私の仕事で使う見積アイテムは、
 バルコニー手摺  アルミ製(ステンカラー)   H=1100
 床       300角磁器質タイル貼
 土間      防水モルタル金鏝押エ    t=30
といったように、見た目を合わせて書くものが多いです。(ここではフォントの関係でずれてますけど)
データベースから作成したものは、自動処理させていますが、手入力する部分は 結構面倒なので、TEditにインデント機能をつけてみようと思います。

今回作成する機能一覧
[ CTRL+I ]
  次のインデント位置に要素を移動させます。
[ CTRL+SHIFT+I ]
  前のインデント位置に要素を移動させます。
[ CTRL+U ]
  次の要素開始位置へキャレットを移動させます。  
[ CTRL+SHIFT+U ]
  前の要素開始位置へキャレットを移動させます。 

いまいち使い方が伝わらないかも知れませんが、編集エディットで上記のキーを試してみて下さい。

Indent


ランタイムテーマにも対応させるため、☆カーソル下のテキストを選択する。で 作成した AdjustedEdit 名前空間を使います。
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, AdjustedEdit;

type
  TPositionList = array of Integer;

  TForm1 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Label1: TLabel;
    Memo1: TMemo;
    Label2: TLabel;
    Label3: TLabel;
    procedure Edit1KeyPress(Sender: TObject; var Key: Char);
  private
    procedure GetIndentPosList(S: String; var List: TPositionList);
    procedure GetElementPosList(S: String; var List: TPositionList);
  public
    procedure PriorIndent;
    procedure NextIndent;
    procedure PriorElement;
    procedure NextElement;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  // Ctrl + Iで指定位置にエレメントを移動させます。
  if (GetKeyState(VK_SHIFT) and $80 = 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #9) then
  begin
    NextIndent;
    Key := #0;
    Exit;
  end;

  // Ctrl + Shift + Iで指定位置にエレメントを移動させます。
  if (GetKeyState(VK_SHIFT) and $80 > 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #9) then
  begin
    PriorIndent;
    Key := #0;
    Exit;
  end;

  // 要素開始位置へキャレットを移動させます。CTRL+U
  if (GetKeyState(VK_SHIFT) and $80 = 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #21) then
  begin
    Key := #0;
    NextElement;
    Exit;
  end;

  // 要素開始位置へキャレットを移動させます。CTRL+SHIFT+U
  if (GetKeyState(VK_SHIFT) and $80 > 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #21) then
  begin
    Key := #0;
    PriorElement;
    Exit;
  end;
end;


procedure TForm1.NextIndent;
var
  P1, P2: Integer;
  I,J: Integer;
  S1, S2, S3: String;
  List: TPositionList;
begin
  SendMessage(Edit1.Handle, WM_SETREDRAW, Ord(False), 0);
  try
    P1 := GetSelStart(Edit1);
    P2 := 0;

    GetIndentPosList(Edit2.Text, List);
    for I := 0 to High(List) do
      if List[I]-1 > P1 then
      begin
        P2 := List[I]-1;
        Break;
      end;

    if P2 = 0 then Exit;

    S1 := Copy(Edit1.Text, 1, P1);
    S2 := Copy(Edit1.Text, P1+1, Length(Edit1.Text)-P1);

    J := P2 - P1;
    S3 := S1 +StringOfChar(' ', J) + S2;
    Edit1.Text := S3;
  finally
    SendMessage(Edit1.Handle, WM_SETREDRAW, Ord(True), 0);
  end;
  SetSelStart(Edit1, P2);
end;

procedure TForm1.PriorIndent;
var
  P1, P2, P3: Integer;
  I,J: Integer;
  S1, S2, S3: String;
  List: TPositionList;
begin
  SendMessage(Edit1.Handle, WM_SETREDRAW, Ord(False), 0);
  try
    P1 := GetSelStart(Edit1);
    P2 := 0;

    GetIndentPosList(Edit2.Text, List);
    for I := High(List) downto 0 do
      if List[I]-1 < P1 then
      begin
        P2 := List[I]-1;
        Break;
      end;

    if P2 < 0 then Exit;

    S1 := TrimRight(Copy(Edit1.Text, 1, P1));
    P3 := Length(S1);

    S2 := Copy(Edit1.Text, P1+1, Length(Edit1.Text)-P1);

    if P3 >= P2 then
    begin
      S3 := S1 + S2;
      P2 := P3;
    end
    else
    begin
      J := P2 - Length(S1);
      S3 := S1 +StringOfChar(' ', J) + S2;
    end;
    Edit1.Text := S3;
  finally
    SendMessage(Edit1.Handle, WM_SETREDRAW, Ord(True), 0);
  end;
  SetSelStart(Edit1, P2);
end;

procedure TForm1.NextElement;
var
  I: Integer;
  P1: Integer;
  List: TPositionList;
begin
  GetElementPosList(Edit1.Text, List);
  P1 := GetSelStart(Edit1);
  for I := 0 to High(List) do
    if P1 < List[I] then
    begin
      P1 := List[I];
      Break;
    end;
  SetSelStart(Edit1, P1);
end;

procedure TForm1.PriorElement;
var
  I: Integer;
  P1: Integer;
  List: TPositionList;
begin
  GetElementPosList(Edit1.Text, List);
  P1 := GetSelStart(Edit1);
  for I := High(List) downto 0 do
    if P1 > List[I] then
    begin
      P1 := List[I];
      Break;
    end;
  SetSelStart(Edit1, P1);
end;

// 要素の位置を取得します。
procedure TForm1.GetElementPosList(S: String; var List: TPositionList);

  procedure Increment(var P: PChar; var I: Integer; Value: Integer);
  begin
    Inc(P, Value);
    Inc(I, Value);
  end;

var
  P: PChar;
  I, J: Integer;
  w: Word;
  F: Boolean;
begin
  SetLength(List, 100); // 100は適当
  P := PChar(S + #0);
  I := 0;
  J := -1;
  F := True;
  while (P^ <> #0) do
  begin
    if IsDBCSLeadByte(Byte(P^)) then
    begin
      w := (Byte(P^) shl 8) or Byte((P+1)^);
      if F and (w <> $8140) then
      begin
        Inc(J);
        List[J] := I;
        Increment(P, I, 2);
        F := False;
      end
      else if (w = $8140) then
      begin
        Increment(P, I, 2);
        F := True;
      end
      else
       Increment(P, I, 2);
    end
    else
    begin
      if F and (P^ <> ' ') then
      begin
        Inc(J);
        List[J] := I;
        Increment(P, I, 1);
        F := False;
      end
      else if (P^ = ' ') then
      begin
        Increment(P, I, 1);
        F := True;
      end
      else
       Increment(P, I, 1);
    end;
  end;
  SetLength(List, J+1);
end;

// インデントの位置を取得します。
procedure TForm1.GetIndentPosList(S: String; var List: TPositionList);
var
  I: Integer;
  SL: TStringList;
begin
  SL := TStringList.Create;
  try
    SL.CommaText := S;
    SetLength(List, SL.Count);
    for I := 0 to SL.Count - 1 do
      List[I] := StrToIntDef(SL[I],0);
  finally
    SL.Free;
  end;
end;

end.

問題点としては、Undoできないことです。よくわかりませんが、文字列を置き換える前に、keybd_event等でクリップボードのカットをさせたりするとできるのかな?

今後、これをTDBGridのTInplaceEditに実装したいと思っています。

|

☆カーソル下のテキストを選択する。

☆メモ上でマウス下のテキスト取得で使った GetWordAndPhrase名前空間を使い、エディットコントロールでカーソル下のテキストを選択させます。

実行させるとこんな感じです。

Undermouse


ここで問題になるのが、TEdit、TMemoは「ランタイムテーマの影響を受けてしまう」ことです。 TRichEditは、ランタイムテーマの影響を受けず、常にバイト数で扱いますが、TEdit、TMemoは ランタイムテーマが有効時-文字数、無効時-バイト数となります。 今回、ランタイムテーマが有効、無効にかかわらず、バイト数で扱う処理を考えてみます。
uses
 GetWordAndPhrase, AdjustedEdit;

procedure TForm1.Edit1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  CursolPos, sp, ep: Integer;
  S: String;
begin
  Exit;
  CursolPos := LoWord(Edit1.Perform(EM_CHARFROMPOS, 0, MakeLParam(X, Y)));
  CursolPos := GetSelStart_Byte(Edit1.Text, CursolPos);
  S := GetWordAndPhraseUnderMouse(CursolPos, Edit1.Text, sp, ep);
  if S <> ''then
  begin
    if GetSelStart(Edit1) <> sp then
    begin
      SetSelStart(Edit1, sp);
      SetSelLength(Edit1, ep-sp+1);
    end;
  end;
  if ActiveControl <> Edit1 then
    ActiveControl := Edit1;
end;

procedure TForm1.Memo1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  CursolPos, sp, ep: Integer;
  S: String;
begin
  CursolPos := LoWord(Memo1.Perform(EM_CHARFROMPOS, 0, MakeLParam(X, Y)));
  CursolPos := GetSelStart_Byte(Memo1.Text, CursolPos);
  S := GetWordAndPhraseUnderMouse(CursolPos, Memo1.Text, sp, ep);
  if S <> ''then
  begin
    if GetSelStart(Memo1) <> sp then
    begin
      SetSelStart(Memo1, sp);
      SetSelLength(Memo1, ep-sp+1);
    end;
  end;
  if ActiveControl <> Memo1 then
    ActiveControl := Memo1;
end;

procedure TForm1.RichEdit1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  CursolPos, sp, ep: Integer;
  S: String;
  Pt:TPoint;
begin
  Pt := Point(X,Y);
  CursolPos := RichEdit1.Perform(EM_CHARFROMPOS, 0, Integer(@Pt));
  S := GetWordAndPhraseUnderMouse(CursolPos, RichEdit1.Text, sp, ep);
  if S <> ''then
  begin
    if RichEdit1.SelStart <> sp then
    begin
      RichEdit1.SelStart := sp;
      RichEdit1.SelLength := ep-sp+1;
    end;
  end;
  if ActiveControl <> RichEdit1 then
    ActiveControl := RichEdit1;
end;


ランタイムテーマの設定にかかわらず、バイト単位で扱うための処理を集めた名前空間 AdjustedEdit (名前が変なのは無視して~)
unit AdjustedEdit;

{ ランタイムテーマの設定にかかわらず、バイト単位で扱います。 }

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  StdCtrls, Buttons, ExtCtrls, Themes;

  // SelTextを取得します。
  function GetSelText(CustomEdit: TCustomEdit): String;

  // SelStartをバイト数で設定します。
  procedure SetSelStart(Edit: TCustomEdit; Position_Byte: Integer);

  // SelStartをバイト数で取得します。
  function GetSelStart(Edit: TCustomEdit): Integer;

  // SelLengthをバイト数で設定します。
  procedure SetSelLength(Edit: TCustomEdit; Length_Byte: Integer);

  // SelLengthをバイト数で取得します。
  function GetSelLength(Edit: TCustomEdit): Integer;

  // 文字数で指定された位置のバイト数を返します。
  function GetSelStart_Byte(S: WideString; Position_Moji: Integer): Integer;

implementation

// 指定位置のバイト数分の文字数を返します。
function GetMoji(S: String; Position: Integer): Integer;
var
  P: PChar;
  I: Integer;
  Check: String;
begin
  if not ThemeServices.ThemesEnabled then
  begin
    Result := Position;
    Exit;
  end;

  Check := Copy(S, 1, Position);
  P := PChar(Check+#0);
  I := 0;
  while (P^ <> #0) do
  begin
    if IsDBCSLeadByte(Byte(P^)) then
      Inc(P,2)
    else
      Inc(P);
    Inc(I);
  end;
  Result := I;
end;

// SelTextを取得します。
function GetSelText(CustomEdit: TCustomEdit): String;
var
  S, Str: WideString;
  I, Start, Len: Integer;
begin
  if not ThemeServices.ThemesEnabled then
  begin
    Result := CustomEdit.SelText;
    Exit;
  end;

  S := WideString(CustomEdit.Text);
  Start := CustomEdit.SelStart + 1;
  Len := Start + CustomEdit.SelLength -1;

  Str := '';
  for I := Start to Len do
    Str := Str + S[I];
  Result := Str;
end;

// SelStartをバイト数で設定します。
procedure SetSelStart(Edit: TCustomEdit; Position_Byte: Integer);
begin
  Edit.SelStart := GetMoji(Edit.Text, Position_Byte);
end;

// SelStartをバイト数で取得します。
function GetSelStart(Edit: TCustomEdit): Integer;
begin
  Result := GetSelStart_Byte(Edit.Text, Edit.SelStart);
end;

// SelLengthをバイト数で設定します。
procedure SetSelLength(Edit: TCustomEdit; Length_Byte: Integer);
var
  I,J: Integer;
begin
  I := GetSelStart(Edit);
  if I = 0 then I := 1;
  J := GetMoji(Edit.Text, I + Length_Byte);
  Edit.SelLength := J-GetMoji(Edit.Text,I);
end;

// SelLengthをバイト数で取得します。
function GetSelLength(Edit: TCustomEdit): Integer;
begin
  Result := Length(GetSelText(Edit));
end;

// 文字数で指定された位置のバイト数を返します。
function GetSelStart_Byte(S: WideString; Position_Moji: Integer): Integer;
var
  Check: String;
begin
  if not ThemeServices.ThemesEnabled then
    Result := Position_Moji
  else
  begin
    Check := Copy(S, 1, Position_Moji);
    Result := Length(Check);
  end;
end;

end.

上記、名前空間にSetSelTextという処理がないのは、次の処理がランタイムの設定に関係なく正常に動作するからです。

  Edit1.Seltext := 'ややこしい!';

SelTextを表示させたら、でたらめなのに、この処理が正常に動作する理由がわかりません。

こんなややこしい処理で悩むのなら、TMemoだけでなく、TEditもTRichEditを次の設定にして代用した方が簡単なのかも知れませんね。

  RichEdit1.WantReturns := False;
  RichEdit1.WordWrap := False;

|

☆EM_CHARFROMPOSでカーソル位置を取得する。

TRichEditとTEdit、TMemoでは、EM_CHARFROMPOSの仕様が違うことを思い出しました。

// TEditの場合(TMemoも同じ)
procedure TForm1.Edit1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  CursolPos: Integer;
begin
  CursolPos := LoWord(Edit1.Perform(EM_CHARFROMPOS, 0, MakeLParam(X, Y)));
end;

// TRichEditの場合
procedure TForm1.RichEdit1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  Pt:TPoint;
  CursolPos: Integer;
begin
  Pt := Point(X,Y);
  CursolPos := RichEdit1.Perform(EM_CHARFROMPOS, 0, Integer(@Pt));
end;

TEdit、TMemoは、ランタイムテーマが有効時-文字数、無効時-バイト数を返しますが、TRichEditは、常にバイト数で返します。


MSDN
EM_CHARFROMPOS Message
http://msdn2.microsoft.com/en-us/library/ms672066.aspx

|

☆標準の編集PopupMenuが・・・??

Delphi2007で作成したプログラムを、知人に渡したのですが、 文字入力部分での右クリックで編集メニューがほしいと言われました。

そのプログラムでは、特に何もしていないので、標準のものがあるはずだし、 おかしいなと思いながら、いろいろ試してみると、標準の編集PopupMenuが出てこないようです。

下記の要領で試してみて下さい。
フォームにTMemo、TEditを配置して、そのまま実行します。
右クリックで出てくるはずのPopupMenuが表示されません。

なんで?



[以下、20070917追記]

調べてみるとこの問題について、既にニュースグループで話題になっていました。

解決方法は、
borland.public.delphi.vcl.componens.using.win32
Tmemo in Delphi 2007 Update3ツリーでの
Peter Below(TeamB)氏のコメントをご覧下さい。

但し、私が試したところでは、TEdit、TMemoは、標準のPopupMenuが表示されましたが、RichEditは、メモリアクセス違反が発生しました。


CodeGear
Delphi Newsgroups
http://support.codegear.com/newsgroups/directory/delphi

borland.public.delphi.vcl.componens.using.win32
Tmemo in Delphi 2007 Update3

borland.public.delphi.ide.general
D2007 Upd3: Lost standart Popup menus for TEdits, TMemos, TRichEdit

|

☆Edit1.SelTextが・・・。

以前、XPmanを貼り付ける等により、XPテーマを有効にした時には、 SelTextの値がおかしくなると書きました。 (APIの仕様変更によるものらしいですが)

Delphi2007では、XPmanを貼り付けなくても、デフォルトでテーマが有効になっています。
既存のアプリケーションをDelphi2007でコンパイルし直す時には、ご注意を・・・。
(エラーが出ないので、なかなかわかりにくい部分ですよね)

SelText部分を書き直す一例としては、下記のような処理ですね。
// XPテーマ有効時、Editで選択された文字列を返します。
function GetSelText(Edit: TEdit): String;
var
  S, Str: WideString;
  I, Start, Len: Integer;
begin
  S := WideString(Edit.Text);
  Start := Edit.SelStart + 1;
  Len := Start + Edit.SelLength -1;

  Str := '';
  for I := Start to Len do
    Str := Str + S[I];
  Result := Str;
end;

ちなみにDelphi2007の場合、XPテーマを無効にするには、メニューの
プロジェクト→アプリケーション →アプリケーションの設定 ランタイムテーマを有効にする
のチェックを外します。

|

☆多数のEditのテキストを削除する。

TFormは、TComponentから派生されていますので、 ComponentsプロパティやComponentCountプロパティを使えば、 フォーム上のコンポーネントの単純な処理が可能になります。
ここでは、Editのテキストを空白にする処理を行ってみました。

procedure TForm1.Button1Click(Sender: TObject);
var
  I: Integer;
begin
  // Editの初期化
  for I := 0 to Self.ComponentCount - 1 do
    if Self.Components[I] is TEdit then
      (Self.Components[I] as TEdit).Text := '';
end;

|

■ComboBox Ctl3D プロパティ

ComboBoxのCtl3Dプロパティは、動作しないので、フラットなコンボボックスは、自作コンポを使っていました。
し、しかし、本日発見してしまいました。ComboBoxにBevelKindというプロパティがあることを。しかもそれを bkFlat にするだけでフラットなコンボボックスにできるということを。
今更何を言ってるのかと言われそうですが、私は全く知りませんでした。
このプロパティ、どのバージョンからあったの?
※ちなみにTEditにもありました。

|

★XPmanコンポ

XPmanコンポをフォームに貼り付けると、Edit1.SelTextの返す値がおかしくなります。
マニフェストを作成した場合でも同じです。
(Delphi7でもDelphi2005でもBDS2006でも)

|