MaxLength をどうにかする (Delphi 7 ~ Delphi 2007)

 このような場合で、OS が Windows XP 以降でテーマが有効だと、TEdit 等の MaxLength が ANSI 文字でみた場合のエレメント単位 (バイト) ではなく、Unicode 文字で見た場合のエレメント単位 (ワード) になってしまう事があります。これを回避するには以下のようなコードを記述します。

procedure TForm1.Edit_Change(Sender: TObject);
var
  MaxLen: Integer;
begin
  MaxLen := SendMessage((Sender as TCustomEdit).Handle, EM_GETLIMITTEXT, 00);
  if MaxLen <= 0 then
    Exit;
  with (Sender as TCustomEdit) do
    begin
      if Length(Text) <= MaxLen then
        Exit;
      if ByteType(Text, MaxLen) = mbLeadByte then
        Dec(MaxLen);
      Text := Copy(Text, 1, MaxLen);
      SelLength := 0;
      SelStart  := MaxLen;
    end;
end;

 この OnChange イベントハンドラをコンポーネントに割り当ててやる事で問題を回避できます。TCustomEdit からの派生コンポーネントであれば、TEdit 以外のコンポーネントにも適用できます (TMemo 等)。

 コンボボックスの場合はどうなるかと言うと...

procedure TForm1.ComboBox_Change(Sender: TObject);
var
  MaxLen: Integer;
begin
  MaxLen := (Sender as TComboBox).MaxLength;
  if MaxLen <= 0 then
    Exit;
  with (Sender as TComboBox) do
    begin
      if Length(Text) <= MaxLen then
        Exit;
      if ByteType(Text, MaxLen) = mbLeadByte then
        Dec(MaxLen);
      Text := Copy(Text, 1, MaxLen);
      SelLength := 0;
      SelStart  := MaxLen;
    end;
end;

 こうなります。TComboBox からの派生コンポーネントであれば、TComboBox 以外のコンポーネントにも適用できます。

 TCustomComboBox でないのは、コンボボックスコントロールに、MaxLength に指定された値を簡単に取得する方法がないからです (エディットコントロールでいうところの EM_GETLIMITTEXT 相当のメッセージが存在しない)。やってやれない事はありませんが、"OnChange というキー入力の度に発生するイベントであまり長いコードを書きたくない" というのが実情です。


特権を有効にする

 NT系の OS では、ちょっとコアな事をやろうとすると特権の設定が必要になる事があります。例えば、ExitWindowsEx() でシャットダウンする時にはシャットダウン特権として、"SE_SHUTDOWN_NAME" を指定しなくてはなりません。Tips には個別の機能に付随して特権の設定が出てくる事がありますが、あえて単独の Tips にしてみます。

const
  SE_ASSIGNPRIMARYTOKEN_NAME  = 'SeAssignPrimaryTokenPrivilege';
  SE_AUDIT_NAME               = 'SeAuditPrivilege';
  SE_BACKUP_NAME              = 'SeBackupPrivilege';
  SE_CHANGE_NOTIFY_NAME       = 'SeChangeNotifyPrivilege';
  SE_CREATE_PAGEFILE_NAME     = 'SeCreatePagefilePrivilege';
  SE_CREATE_PERMANENT_NAME    = 'SeCreatePermanentPrivilege';
  SE_CREATE_TOKEN_NAME        = 'SeCreateTokenPrivilege';
  SE_DEBUG_NAME               = 'SeDebugPrivilege';
  SE_INC_BASE_PRIORITY_NAME   = 'SeIncreaseBasePriorityPrivilege';
  SE_INCREASE_QUOTA_NAME      = 'SeIncreaseQuotaPrivilege';
  SE_LOAD_DRIVER_NAME         = 'SeLoadDriverPrivilege';
  SE_LOCK_MEMORY_NAME         = 'SeLockMemoryPrivilege';
  SE_MACHINE_ACCOUNT_NAME     = 'SeMachineAccountPrivilege';
  SE_PROF_SINGLE_PROCESS_NAME = 'SeProfileSingleProcessPrivilege';
  SE_REMOTE_SHUTDOWN_NAME     = 'SeRemoteShutdownPrivilege';
  SE_RESTORE_NAME             = 'SeRestorePrivilege';
  SE_SECURITY_NAME            = 'SeSecurityPrivilege';
  SE_SHUTDOWN_NAME            = 'SeShutdownPrivilege';
  SE_SYSTEM_ENVIRONMENT_NAME  = 'SeSystemEnvironmentPrivilege';
  SE_SYSTEM_PROFILE_NAME      = 'SeSystemProfilePrivilege';
  SE_SYSTEMTIME_NAME          = 'SeSystemtimePrivilege';
  SE_TAKE_OWNERSHIP_NAME      = 'SeTakeOwnershipPrivilege';
  SE_TCB_NAME                 = 'SeTcbPrivilege';
  SE_UNSOLICITED_INPUT_NAME   = 'SeUnsolicitedInputPrivilege';


// 特権を変更する
// -----------------------------------------------------------------------------
// szPrivilege: システムを指定する特権文字列
// aEnabled   : 特権の有効(True) / 無効(False) を指定
// 戻り値     : 成功なら True, 失敗なら False
// -----------------------------------------------------------------------------
function SetPrivilege(szPrivilege: String; aEnabled: Boolean): Boolean;
var
  TokenHandle: THandle;
  NewState: TTokenPrivileges;
  ReturnLength: Cardinal;
begin
  result := False;
  if OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES, TokenHandle) then
    begin
      if LookupPrivilegeValue(nil, PChar(szPrivilege), NewState.Privileges[0].Luid) then
        begin
          NewState.PrivilegeCount := 1;
          if aEnabled then
            begin
              NewState.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
              result := AdjustTokenPrivileges(TokenHandle, False, NewState, SizeOf(NewState), nil, ReturnLength);
            end
          else
            begin
              NewState.Privileges[0].Attributes := 0;
              result := AdjustTokenPrivileges(TokenHandle, True, NewState, 0nil, ReturnLength);
            end;
        end;
      CloseHandle(TokenHandle);
    end;
end;

 ExitWindowsEx() を用いて PC をシャットダウンする場合には以下のようになります

  // シャットダウン特権を与える
  SetPrivilege(SE_SHUTDOWN_NAME, True);
  // シャットダウンを行う
  ExitWindowsEx(EWX_SHUTDOWN, 0);

別の HDD にあるレジストリ(ハイブファイル)を操作する

 少々マニアックなネタですが... PC が起動しない際、"このレジストリエントリを書き換えれば動作するのに!" という事がたまにあります。 このような状況に陥っている場合には、VGA モードやセーフモードですら起動せず、問答無用のBSoD を拝んでいる状況でしょう。 本 Tips は、そういったシステム復旧時のレジストリ操作についてです。

 想定しているのは、図のように別の PC (PC-B) のレジストリエントリがおかしくなって起動しない場合です。マルチブート環境で、とある OS のみ起動しなくなった場合でも 本Tips は有効です。 とりあえず、PC-B の HDD をどうにかして PC-A から読めるような状態にして下さい。USB でも eSATA でも結構です。PC-B の HDD は PC-A から D: ドライブとして見えている事にします。

別の HDD にあるレジストリ(ハイブファイル)を操作するには?

 マニュアルでやる方法として、regedit.exe を紹介してみます。簡単な復旧であれば、レジストリエディタがあればどうにかなります。まずレジストリエディタ (regedit.exe) を起動し、HKEY_LOCAL_MACHINE を選択状態にします。

 次に [ファイル | ハイブの読み込み] を行います。

 読み込むべきファイルは D: ドライブの "%SystemRoot%\System32\Config" にあります。ここにあるファイルが PC-B のレジストリハイブファイルです。以下に、レジストリキーとファイルの対応表を掲載します。

レジストリキー ファイル名
[HKEY_LOCAL_MACHINE\SAM] %SystemRoot%\System32\Config\SAM
[HKEY_LOCAL_MACHINE\Security] %SystemRoot%\System32\Config\SECURITY
[HKEY_LOCAL_MACHINE\Software] %SystemRoot%\System32\Config\SOFTWARE
[HKEY_LOCAL_MACHINE\System]
[HKEY_CURRENT_CONFIG]
%SystemRoot%\System32\Config\SYSTEM
[HKEY_USERS\.DEFAULT] %SystemRoot%\System32\Config\DEFAULT
[HKEY_CURRENT_USER] %UserProfile%\NTUSER.DAT

 ハードウェア関連のレジストリが不正で PC が起動しない場合には、SYSTEM を読み込む事になります (大抵、用があるのは [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet] でしょうから)。キー名に "maint_SYSTEM" と名前を付けてみます。名前は任意で構わないのですが、分り易い名前がいいと思います。

 こうして PC-A の [HKEY_LOCAL_MACHINE] の下に、PC-B のレジストリ ([HKEY_LOCAL_MACHINE\System]) が読み込まれました。PC-B の [HKEY_LOCAL_MACHINE\SYSTEM] を [HKEY_LOCAL_MACHINE\maint_SYSTEM] としてアクセスできるようになった訳です。

 ...ところがどっこい。[HKEY_LOCAL_MACHINE] をいじろうとして困った問題に直面すると思います。[HKEY_LOCAL_MACHINE\maint_SYSTEM] に "CurrentControlSet" が存在しないからです。代わりに ControlSet001~ が複数存在する事でしょう。

 簡単に言えば、これらのコントロールセットのいずれかが "カレントコントロールセット" な訳です。どのコントロールセットがカレントだったのかは [HKEY_LOCAL_MACHINE\maint_SYSTEM\Select] にある "Current" を調べると判ります。ここに 1 が指定されていた場合には "ControlSet001" が CurrentControlSet です。2 なら同様に "ControlSet002" が CurrentControlSet となります。

 一通りレジストリ操作が終わったら、[HKEY_LOCAL_MACHINE\maint_SYSTEM] を選択状態にして、[ファイル | ハイブのアンロード] でハイブファイルをアンロードしておきます。

それを Delphi からやるには?

 やっと本題に入ります。ぶっちゃけ、ハイブファイルの操作を Delphi からやる事は滅多にないでしょう。ですが、レジストリエディタでの作業は大量のレジストリエントリ操作には向いていません。ここはひとつ Delphi でやってみましょう。

const
  HIVE_KEY_NAME = 'maint_SYSTEM';
var
  Reg: TRegistry;
  HiveFileName: String;
begin
  // レジストリハイブ操作特権を設定
  SetPrivilege(SE_RESTORE_NAME, True);
  // レジストリ操作
  Reg := TRegistry.Create;
  try
    // ハイブファイルをロード
    Reg.RootKey    := HKEY_LOCAL_MACHINE;
    HiveFileName := 'D:\Windows\System32\Config\SYSTEM';
    Reg.LoadKey(HIVE_KEY_NAME, HiveFileName);
    try

      // (処理)

    finally
      // ハイブファイルをアンロード
      Reg.UnLoadKey(HIVE_KEY_NAME);
    end;
  finally
    Reg.Free;
    SetPrivilege(SE_RESTORE_NAME, False);
  end;
end;

 前置きが長かったですが、たったこれだけだったりします。"Reg.LoadKey には特権が必要" というのだけが注意点です。SetPrivilege() については "89.特権を有効にする" を参考にして下さい。また、レジストリ関連の設定は阿久津さんの記事が役に立つと思います。ぜひご一読を。

See Also:

 BACK