본문 바로가기
Application/Delphi Lecture

한글 입력기 구현

by 현이빈이 2008. 7. 24.
반응형

한글 입력기 구현

목표:

컴퓨터상에서 한글을 입력하기 위해서는 한글입력기라는 특수한 알고리즘이 필요합니다. 한글의 경우 영문과 달리 코드를 조합하여 한 글자가 만들어지는 방식이기 때문입니다. 예를 들어 "한" 이라는 글자를 완성하려면, 영문 키에서 g-k-s 의 세 키를 눌러야 하죠. 이렇게 눌려진 g-k-s 키의 값들이 어떠한 알고리즘에 의해서 "한"이라는 글자로 변환되는 것입니다. 여기서는 Windows 의 네이티브 한글 입력기를 사용하지 않고 한글을 입력할 수 있는 한글 입력기 알고리즘을 구현해 보겠습니다.

배경지식:

먼저 초성-중성 형태의 한글 입력만을 위한 한글 입력기를 생각해 봅시다. 어떻게 만들까요? 물론 수많은 방법이 있지만 저는 이렇게 만들어 보겠습니다. 전산 용어로 정확히 어떻게 되는지는 기억이 나지 않습니다만, 다음은 현재 상태와 사건의 관계를 나타낸 그림입니다. 잘 모르지만 그냥 상태 다이아그램이라고 해 두지요. (이 그림, 좀 귀엽지 않습니까?)
 

위의 그림에서 동그라미와 A, B가 쓰여 있는 것은 현재 상태를 나타냅니다. 화살표는 현재 상태에서 발생한 사건(event)이고, 화살표가 가리키는 것은 그 사건에 대해서 옮겨지는 상태를 나타냅니다. 초기상태와 최종 상태는 A이고 other는 나머지 사건들을 제외한 모든 사건을 나타냅니다.

먼저 "가리라" 라는 글자의 입력 순서를 보시죠.

가리라: A > ㄱ > B > ㅏ > A > ㄹ > B > ㅣ > A > ㄹ > B > ㅏ > A

"초성-중성"이 반복되었으므로 현재 상태는 계속 A>B>A>B>... 를 반복하게 됩니다. 그렇다면 다른 경우인 "이X야"의 경우는 입력 순서가 다음과 같습니다. ('X'는 other 에 해당된다고 보면 되겠죠?)

이X야: A > ㅇ > B > ㅣ > A > X > A > ㅇ > B > ㅑ > A

예외적인 경우인 "초성-초성" 반복이나 "중성-중성" 반복의 경우도 위의 상태 다이어그램으로 동작이 가능하다는 것을 알 수 있습니다.

가ㄹ매기: A > ㄱ > B > ㅏ > A > ㄹ > B > ㅁ > B > ㅐ > A > ㄱ > B > ㅣ > A
오ㅏ: A > ㅇ > B > ㅗ > A > ㅏ > A

여기서 한글 입력시에는 "조합중인 글자"와 "조합이 끝난 글자"의 차이가 있음을 유념해야 할 것입니다. 우리가 한글을 입력할 때 조합중인 글자는 백스페이스로 글자 조합을 변경할 수 있죠? 그러나 이미 조합이 끝난 글자는 한 글자 전체가 삭제됩니다. 조합중인 글자는 대개의 경우 실제로 입력창에 삽입되지 않으며, 위의 상태 다이아그램에서 보았을 때 완전히 조합이 끝날 다음에 실제로 삽입됩니다. (내용에 비해 설명이 너무 어렵군요.)

그럼 위의 상태 다이아그램을 구현한 코드를 한번 보겠습니다. (pseudo-code 형태로 되어 있습니다.)

var
  상태 = A, 초;

// insert(x): 입력창에 x를 삽입한다.
// combine(a, b): 초성a,중성b를 조합한다.

procedure KeyEvent(입력키);
begin
  case 상태 of
    A: case 입력키 of
       초성: 
         상태 := B;
         초 := 입력키;
     else
       insert(입력키);
     end;
    B: case 입력키 of
       초성:
         insert(초);
         초 := 입력키;
       중성: 
         상태 := A;
         insert(combine(초, 입력키)); 
     else
       insert(입력키);
     end;  
  end;   
end;
어려워 보였던 알고리즘이 이렇게 간결하게 완성되었군요!! "초"라는 변수의 쓰임을 잘 보아 주십시오. 이 변수는 "조합중인 글자"를 나타냅니다. 상태 다이아그램을 이용한 프로그래밍은 아주 유용할 경우가 많습니다. 특히 통신 프로토콜을 구현할 때 위와 같이 상태 다이아그램을 작성한 후 이를 코딩하면, 아주 훌륭한 프로그램이 됩니다. 단 작성하는 상태 다이아그램은 모든 경우를 고려하여 빈틈없이 완벽한 것이어야 하겠죠.

위와 같은 방법을 사용해서 모든 형태의 한글조합이 가능한 한글 입력기를 만들어 보겠습니다. 아래의 코드는 한글 입력기를 오브젝트 형태로 구현한 것입니다. 사용방법은 다음과 같습니다.
 

  • 입력기 오브젝트를 생성한다. (a := THanInput.Create)
  • 초기화 (a.Init)
  • 입력 이벤트 발생시에 해당 키를 입력기로 삽입한다. (b := a.Insert(key)) 'ㄱ'의 경우 2벌식 자판의 해당 영문자인 'r'를 삽입하게 된다. 이때 리턴되는 값은 조합이 완료된 글자를 의미한다.
  • State 속성은 현재 조합중인 글자를 의미한다. 만일 방향키가 눌려지거나 마우스 버튼 클릭등 강제적으로 조합이 중단될 경우는 State 속성을 입력창에 삽입하고, 초기화를 실행한다.
  • 여러 문자열을 동시에 입력하는 경우 InsertStr 함수를 사용한다.
  • 사용이 끝나면 오브젝트를 없앤다. (a.Free)

코드:

unit HanInput;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  THanInput = class(TComponent)
  private
    FInputState: Integer;
    FBuff, FComposing: String;
    function FindIndex(Key, Map: String; L: Integer; var Idx: Integer): Boolean;
    function Included(Key, Map: String; L: Integer): Boolean;
  public
    procedure Init;
    function HanDiv(const Han: PChar; Han3: PChar): Boolean;
    function HanCom(const Han3: PChar; Han: PChar): Boolean;
    function HanDivPas(const Src: String): String;
    function HanComPas(const Src: String): String;
    procedure Translate(Src: String; var Dest: String);
    function Insert(var Key: Char): String;
    function InsertStr(Key: String): String;
    constructor Create(AOwner: TComponent); override;
    property State: Integer read FInputState;
    property Composing: String read FComposing;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Jounlai', [THanInput]);
end;

function THanInput.HanDiv(const Han: PChar; Han3: PChar): Boolean;
const
  ChoSungTbl:  PChar = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
  JungSungTbl: PChar = 'ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ';
  JongSungTbl: PChar = '  ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ';
  UniCodeHangeulBase = $AC00;
  UniCodeHangeulLast = $D79F;
var
  UniCode: Integer;
  ChoSung, JungSung, JongSung: Integer;
begin
  Result := False;

  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, Han, 2, @UniCode, 1);

  if (UniCode < UniCodeHangeulBase) or
     (UniCode > UniCodeHangeulLast) then Exit;

  UniCode := UniCode - UniCodeHangeulBase;
  ChoSung := UniCode div (21 * 28);
  UniCode := UniCode mod (21 * 28);
  JungSung := UniCode div 28;
  UniCode := UniCode mod 28;
  JongSung := UniCode;

  StrLCopy(Han3, ChoSungTbl + ChoSung * 2, 2);
  StrLCopy(Han3 + 2, JungSungTbl + JungSung * 2, 2);
  StrLCopy(Han3 + 4, JongSungTbl + JongSung * 2, 2);

  Result := True;
end;

function THanInput.HanCom(const Han3: PChar; Han: PChar): Boolean;
const
  ChoSungTbl:  PChar = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
  JungSungTbl: PChar = 'ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ';
  JongSungTbl: PChar = '  ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ';
  UniCodeHangeulBase = $AC00;
  UniCodeHangeulLast = $D79F;
var
  UniCode: Integer;
  ChoSung, JungSung, JongSung: Integer;
  ChoSungPos, JungSungPos, JongSungPos: Integer;
begin
  Result := False;

  ChoSungPos := Pos(Copy(String(Han3), 1, 2), ChoSungTbl);
  JungSungPos := Pos(Copy(String(Han3), 3, 2), JungSungTbl);
  JongSungPos := Pos(Copy(String(Han3), 5, 2), JongSungTbl);

  if (ChoSungPos and JungSungPos and JongSungPos) = 0 then Exit;

  ChoSung := (ChoSungPos - 1) div 2;
  JungSung := (JungSungPos - 1) div 2;
  JongSung := (JongSungPos - 1) div 2;

  UniCode := UniCodeHangeulBase +
    (ChoSung * 21 + JungSung) * 28 + JongSung;

  WideCharToMultiByte(CP_ACP, WC_COMPOSITECHECK,
    @UniCode, 1, Han, 2, nil, nil);

  Result := True;
end;

function THanInput.HanDivPas(const Src: String): String;
var
  Buff: array[0..6] of Char;
begin
  Result := '';
  if Length(Src) = 2 then begin
    if HanDiv(PChar(Src), Buff) then begin
      Buff[6] := #0;
      Result := String(Buff);
    end;
  end;
end;

function THanInput.HanComPas(const Src: String): String;
var
  Buff: array[0..2] of Char;
begin
  Result := '';
  if Length(Src) = 6 then begin
    if HanCom(PChar(Src), Buff) then begin
      Buff[2] := #0;
      Result := String(Buff);
    end;
  end;
end;

function THanInput.FindIndex(Key, Map: String; L: Integer; var Idx: Integer): Boolean;
var
  I: Integer;
begin
  Idx := -1;
  Result := True;
  for I := 0 to Length(Map) div L - 1 do begin
    if Copy(Map, I * L + 1, L) = Copy(Key, Length(Key) - L + 1, L) then begin
      Idx := I;
      Exit;
    end;
  end;
  Result := False;
end;

function THanInput.Included(Key, Map: String; L: Integer): Boolean;
var
  Idx: Integer;
begin
  Result := FindIndex(Key, Map, L, Idx);
end;

procedure THanInput.Translate(Src: String; var Dest: String);
const
  HanConsonants = 'rRseEfaqQtTdwWczxvg';
  HanVowels = 'koiOjpuPhynbml';
  HanDblConsonants = 'rtswsgfrfafqftfxfvfgqt';
  HanDblVowels = 'hkhohlnjnpnlml';

  HanConsonantsMap = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
  HanVowelsMap = 'ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅛㅜㅠㅡㅣ';
  HanDblConsonantsMap = 'ㄳㄵㄶㄺㄻㄼㄽㄾㄿㅀㅄ';
  HanDblVowelsMap = 'ㅘㅙㅚㅝㅞㅟㅢ';
var
  SrcDiv, Tmp: String;
  Idx, I: Integer;
begin
  Dest := '';
  for I := 0 to Length(Src) div 5 - 1 do begin
    SrcDiv := Copy(Src, I * 5 + 1, 5);
    if SrcDiv[1] = '&' then begin
      if FindIndex(SrcDiv[2], HanConsonants, 1, Idx) then
        Dest := Dest + Copy(HanConsonantsMap, Idx * 2 + 1, 2)
      else if FindIndex(SrcDiv[2], HanVowels, 1, Idx) then
        Dest := Dest + Copy(HanVowelsMap, Idx * 2 + 1, 2);
      Continue;
    end;
    if SrcDiv[1] = '.' then Continue;

    FindIndex(SrcDiv[1], HanConsonants, 1, Idx);
    Tmp := Copy(HanConsonantsMap, Idx * 2 + 1, 2);
    if SrcDiv[3] = '.' then begin
      if SrcDiv[2] = '.' then begin
        Dest := Dest + Tmp;
        Continue;
      end;
      FindIndex(SrcDiv[2], HanVowels, 1, Idx);
      Tmp := Tmp + Copy(HanVowelsMap, Idx * 2 + 1, 2);
    end else begin
      FindIndex(Copy(SrcDiv, 2, 2), HanDblVowels, 2, Idx);
      Tmp := Tmp + Copy(HanDblVowelsMap, Idx * 2 + 1, 2);
    end;
    if SrcDiv[5] = '.' then begin
      if SrcDiv[4] = '.' then begin
        Tmp := Tmp + '  ';
      end else begin
        FindIndex(SrcDiv[4], HanConsonants, 1, Idx);
        Tmp := Tmp + Copy(HanConsonantsMap, Idx * 2 + 1, 2);
      end;
    end else begin
      FindIndex(Copy(SrcDiv, 4, 2), HanDblConsonants, 2, Idx);
      Tmp := Tmp + Copy(HanDblConsonantsMap, Idx * 2 + 1, 2);
    end;
    Dest := Dest + HanComPas(Tmp);
  end;
end;

function THanInput.Insert(var Key: Char): String;
const
  HanConsonants = 'rRseEfaqQtTdwWczxvg';
  HanVowels = 'koiOjpuPhynbml';
  HanDblConsonants = 'rtswsgfrfafqftfxfvfgqt';
  HanDblVowels = 'hkhohlnjnpnlml';
var
  K, Res: String;
  OrgKey: Char;
begin
  K := String(Key);
  OrgKey := Key;
  Key := #0;
  Result := '';
  case FInputState of
    0: begin
      if Included(K, HanVowels, 1) then begin
        Res := '&' + K + '...';
        Translate(Res, Result);
        FBuff := '';
      end else if Included(K, HanConsonants, 1) then begin
        FInputState := 1;
        FBuff := FBuff + K;
      end else begin
        Key := OrgKey;
      end;
    end;
    1: begin
      if Included(K, HanConsonants, 1) then begin
        Res := '&' + FBuff + '...';
        Translate(Res, Result);
        FBuff := K;
      end else if Included(K, HanVowels, 1) then begin
        FInputState := 3;
        FBuff := FBuff + K;
      end else begin
        if OrgKey = #8 then begin
          FInputState := 0;
          FBuff := '';
        end else begin
          Res := '&' + FBuff + '...';
          Translate(Res, Result);
          Key := OrgKey;
          FBuff := '';
        end;
      end;
    end;
    3: begin
      if Included(K, HanConsonants, 1) then begin
        FInputState := 4;
        FBuff := FBuff + '.' + K;
      end else if Included(FBuff + K, HanDblVowels, 2) then begin
        FInputState := 5;
        FBuff := FBuff + K;
      end else if Included(K, HanVowels, 1) then begin
        FInputState := 0;
        Res := FBuff + '...&' + K + '...';
        Translate(Res, Result);
        FBuff := '';
      end else begin
        if OrgKey = #8 then begin
          FInputState := 1;
          FBuff := Copy(FBuff, 1, 1);
        end else begin
          FInputState := 0;
          Res := FBuff + '...&';
          Translate(Res, Result);
          Key := OrgKey;
          FBuff := '';
        end;
      end;
    end;
    4: begin
      if Included(K, HanVowels, 1) then begin
        FInputState := 3;
        Res := Copy(FBuff, 1, 3) + '..';
        Translate(Res, Result);
        FBuff := String(FBuff[4]) + K;
      end else if Included(FBuff + K, HanDblConsonants, 2) then begin
        FInputState := 6;
        FBuff := FBuff + K;
      end else if Included(K, HanConsonants, 1) then begin
        FInputState := 1;
        Res := FBuff + '.';
        Translate(Res, Result);
        FBuff := K;
      end else begin
        if OrgKey = #8 then begin
          if FBuff[3] = '.' then begin
            FInputState := 3;
            FBuff := Copy(FBuff, 1, 2);
          end else begin
            FInputState := 5;
            FBuff := Copy(FBuff, 1, 3);
          end;
        end else begin
          FInputState := 0;
          Res := FBuff + '.';
          Translate(Res, Result);
          Key := OrgKey;
          FBuff := '';
        end;
      end;
    end;
    5: begin
      if Included(K, HanVowels, 1) then begin
        FInputState := 0;
        Res := FBuff + '..&' + K + '...';
        Translate(Res, Result);
        FBuff := '';
      end else if Included(K, HanConsonants, 1) then begin
        FInputState := 4;
        FBuff := FBuff + K;
      end else begin
        if OrgKey = #8 then begin
          FInputState := 3;
          FBuff := Copy(FBuff, 1, 2);
        end else begin
          FInputState := 0;
          Res := FBuff + '..';
          Translate(Res, Result);
          Key := OrgKey;
          FBuff := '';
        end;
      end;
    end;
    6: begin
      if Included(K, HanVowels, 1) then begin
        FInputState := 3;
        Res := Copy(FBuff, 1, 4) + '.';
        Translate(Res, Result);
        FBuff := String(FBuff[5]) + K;
      end else if Included(K, HanConsonants, 1) then begin
        FInputState := 1;
        Res := FBuff;
        Translate(Res, Result);
        FBuff := K;
      end else begin
        if OrgKey = #8 then begin
          FInputState := 4;
          FBuff := Copy(FBuff, 1, Length(FBuff) - 1);
        end else begin
          FInputState := 0;
          Res := FBuff;
          Translate(Res, Result);
          Key := OrgKey;
          FBuff := '';
        end;
      end;
    end;
  end;
  Translate(FBuff + Copy('.....', 1, 5 - Length(FBuff)), FComposing);
end;

procedure THanInput.Init;
begin
  FBuff := '';
  FInputState := 0;
  FComposing := '';
end;

function THanInput.InsertStr(Key: String): String;
var
  I: Integer;
begin
  Result := '';
  for I := 1 to Length(Key) do begin
    Result := Result + Insert(Key[I]);
  end;
end;

constructor THanInput.Create(AOwner: TComponent);
begin
  inherited;
  Init;
end;

end.
반응형