『게임 제작 강좌-프로그래밍 강좌 (go NGM)』 352번 제 목:[옮김] 한우진: 한글 입력 오토마타 올린이:cpascal (변동호 ) 97/10/16 23:17 읽음:726 관련자료 없음 ----------------------------------------------------------------------------- 안녕하세요? 한우진님께서 하이텔 두루물(mul)에 올리셨던 강좌들을 NGM에 올려도 된다는 허락을 받고 올리는 것입니다. ======================================================================= #80 한우진 (hwjketel) 한글 입력 오토마타 의 설명 1 01/23 10:48 260 line 안녕하세요..오랜만에 다시 여러분을 찾아뵙게 되는군요.. 허르미 입니다..이번부터는 한글 입력루틴의 오토마타 를 살펴보도록 하지요.. 한글 입력루틴의 오토마타는 공개되어 있지만 이것만으로는 프로그램하기가 용이치 않습니다..왜냐하면 INS,DEL,BS 등의 키구현이 상당히 까다롭고 커서 처리도 뒤따라야 하기 때문입니다. 우선 적합한 한글 출력루틴이 있어야 테스트를 해볼수 있겠죠... 출력루틴에서 houttextxy 라는 함수를 지원한다고 가정하고 강좌를 출발합시다. 우선 입력루틴의 기본 설계를 합시다. 한글 입렇쨌ㅤㅤㅞㄵ씬 우선 한/영 전환 키이가 필요하고,INS,DEL 키이.. 또 BS 키이.. HOME,END 키이..이 정도만 하지요...참.커서키도.. 입력형식은 나중의 에디터등을 고려하여 문자열을 받아오면 그 문자열을 일단 표시한뒤 그 문자열을 기초로 수정작업을 하는 형 식을 취하도록 합시다. 문자열이 널(아무것도 없음) 이면 보통 생각 하는 입력루틴의 역할을 수행하겠죠.. 그래서 다시 바뀐 문자열을 리턴해 주는 것입니다. 바뀐 문자열은 원래 확보된것보다 더 늘어나서 기존의 영역을 파괴할 위험이 있으므로 입력루틴에 n 자만큼 받아들 이겠낯ㅤㅤㅑㅆㅤㅤㅑㅄ 인자를 사용합니다..한글 한자는 2 자로 취급됩니다.. 이는 나중의 한글 윈도우에서 화면을 깨지 않게 하는데도 필요합니다. 그럼 형식은 hgetsxy(int x,int y,char *str,int length); 이렇게 됩니다. 보다 프로그래밍을 쉽게 하기 위해서 str 은 0 으로 초기화 되어 있는것 으로 가정합시다.. 이 방법의 장점은 커서키이의 이동을 보다 자유롭게 하는데 있읍니다.. 제일 처음에 n 자만큼 str 을 표시할때 malloc 으로 확보한 영역은 초기화 되어있지 않으므로 그냥 표시하면 무지무지 이상 한 문자가 나타나게 되므로 calloc 를 사용하는것이 좋습니다... 배열로 잡아도 y(1)); if ((ch=getch())==0) return (256+getch()); return(ch); } if (!bioskey(1)) return NOKEY; if ((ch=getch())==0) return (256+getch()); return(ch); } bioskey(1) 은 아무 키이도 눌리지 않았다면 0 을 리턴합니다... 위의 mode 는 no_wait 모드를 지원하기 위한 것으로 getch() 가 버퍼에서 한문자 가져오는 역할을 하는것을 안다면 위의 프로그램이 쉽게 이해가 가리라 믿습니다. ch=inkey(WAIT); 하면 키이가 눌릴때까존 기다리다가 눌리면 버퍼의 첫문자가 0 이면 그다음 문자의 아스키 코드에 256 을 더해 리턴하고 아니면 그냥 그 문자를 리턴합니다. ch=inkey(WAIT 이외의 아무거나); 를 하시면 키이가 눌리지 않은 상 태면 그냥 NOKEY 를 리턴하고 눌렸으면 역시 첫문자가 0 인지를 검사하고 위같은 처리를 해줍니다.. 입력루틴에서 써먹을 모드는 WAIT 입니다... 적당한 처리로 위의 while 문안에 커서 반전 루틴을 넣으면 커서가 깜박거릴겁니다.지금은 빼놓도록 하죠. 그럼 이제 문제는 한글 오토마타만 남았네요. 이를 위해선 한글의 조합규칙에 대해 조금 알아볼 필요가 있읍니다.. 두벌식에 대해 생각해보면( 세벌식이 좋다지만 ) 키보드에 있는 A 오른쪽에 미음이 써있을 겁니다.. B 오른쪽에는 유 가 요.. 그럼 이런 대응표가 하나 필요할겁니다.... 한글 모드임을 나타내는 전역변수 플래그를하나 마련하여 토글키이가 눌리면 전환하도록 하고... 한글 모드이면 A 를 미음으로 인식할수 있도록 하는 거죠. 토글은 SHIFT-SPACE 로 합니다. 이 키이의 조사 여부는 bioskey(2); 가 해결해 줍니다.. 이 키이는 몇가지의 특수 키이의 눌림 여부를 조사해 줍니다. /* shift flag */ #define RIGHT_SHIFT 1 #define LEFT_SHIFT 2 #define CTRL 4 #define ALT 8 #define SCROLL_LOCK 16 #define NUM_LOCK 32 #define CAPS_LOCK 64 #define INSERT 128 다음은 그 키이의 눌림 여부를 조사하기 위한 것입니다.. ch=bioskey(2); 는 ch 에 위의 값의 OR 값을 리턴합니다. 곧 각 비트에 대응하는 셈인데 if (입력된것이 스페이스이고) if (ch & RIGHT_SHIFT ch &LEFT_SHIFT) 토글. 이렇게 하면 되겠죠 ? 한글 입력루틴의 오토마타는 몇개의 표가 필요한데요... 이 표를 만드는 방법은 여러가지가 있을수 있을겁니다. 저는 이 표를 다음과 같이 분류했읍니다. 우선 키보드-한글 자모 대응표 .. 이건 꼭 있어야 되겠죠 ? 초성-종성 대응표 .. 키보드의 미음이 눌리면 초성또는 종성의 코드를 찾아 줍니다..두가지 다 가능성이 있으 니까요. 복모음 대응표 ..곧 '오' 랑 '아' 면 '와' 가 된 다는 표를 말합니다. 복자음 대응표 .. 끝에 '삶' 과 같은 경우 리을+미음=리을미음 이 된다는 이 표를 만들기 위해 한글 코드값에 따른 한글 자모를 봅시다. 한글 2 바이트 코드에서 왼쪽부터 1,5,5,5 비트씩 갈라서 더미,초성, 중성,종성으로 가른다는것은 이미 설명했고요... 구조체와 공용체 를 이용해서 쉽게 가를수 있다는 것도 설명했읍니다. typedef struct { unsigned lasc :5; unsigned midc :5; /* HAN CODE FORM */ unsigned firc :5; unsigned dummy:1; } hcode; typedef struct { unsigned char sbyte; /* HAN BYTE FORM */ unsigned char fbyte; } hbyte; union { hcode c; hbyte b; } hanin; 위와 같다면 hanin.c.firc 면 초성 코드값, hanin.b.fbyte 면 2 바이트에 서 앞의 바이트의 값이 들어옵뉨ㅤㅤㅝㅄ.. 따라서 입력루틴에서는 hanin.c.~~ 를 이용해서 초/중/종성을 다루면서 출력루틴에는 hanin.b.~~ 를 넘기면 되는 것이죠.. 이렇게 갈라진 hanin.c.firc 등의 값에 따른 분류입니다..완성형케텔이라 좀 어지러우시겠지만 양해해 주세요. 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1.초성: 가 까 나 다 따 라 마 바 빠 사 싸 아 자 짜 차 카 타 파 하 3 4 5 6 7 10 11 12 13 14 15 18 19 20 21 22 23 26 27 2.중성: 아 애 야 얘 어 에 여 예 오 와 왜 외 요 우 워 웨 위 유 으 28 29 의 이 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 19 20 21 3.종성: 악 c d 안 앉 않 h 알 앍 앎 l m n o 앓 암 압 t 앗 22 23 24 25 26 27 28 29 았 앙 x y z 앝 앞 } 위의 값은 별도의 코드 참조표에의해 참조되어 실제 무슨 글자인지를 알게 됩니다. 그 참조표는 위의 값들의 순서를 담고 있읍니다... 곧 초성의 경우 가 면 1 을 까 면 2 를 이런식으로요 ,위에는 연속적 이지 않은 값들이 있지만 그 표에 의해 바꾸면 그냥 위의 순서대로 참조가 가능하죠. 그 표뉨 출력루틴에서도 써먹었던 것입니다.. 벌수를 결정할때 였죠... 참고하시면 도움이 될겁니다. 그럼 표를 만들어 봅시다.. 우선 쉬프트가 눌리지 않은 키-자모 표. 이 표눈 자음의 경우 초성값을 가지고 있읍니다.종성으로 바꿀때는 뒤의 초성-종성 대응표를 이용합니다. byte key [26]={ 8,126,16,13,5,7,20,113,105,107,103,129,127, 120,104,110,9,2,4,11,111,19,14,18,119,17 }; 표에서 보면 8 이 위의 초성에의 미음에 대응합니다. 126 은 위의 표에 없는데 이는 실은 100 을 더한 값입니다...꼭 100 이중요한건 아니고요 자/모음을 분별하기 위한 것입니다. 그러니까 위의 중성표의 26,즉 유에 해당합니다.. 이런식으로 계속됩니다..그다음은 쉬프트가 눌린값입니다. byte key_shifted[26]={ 8,126,16,13,6,7,20,113,105,107,103,129,127, 120,106,112,10,3,4,12,111,19,15,18,119,17 }; 그다음은 초성-중성 찾기 표.. byte first_last[16][2]={{2,2},{3,3},{4,5},{5,8},{7,9},{8,17},{9,19}, {11,21},{12,22},{13,23},{14,24},{16,25}, {17,26},{18,27},{19,28},{20,29} }; 이건 자세히 보시면 앞의 인자는 초성,뒤의 인자는 그와 같은 종성입 니다. 그러니까 2,2 에서 2 는 초성의 기역,뒤의 2 는 종성의 기역입니 다. 반드시 일치하지는 않습니다... 4,5 에서 4 는 초성의 니은, 5 는 종성의 니은 입니다.. 이제 두개 남은 표는 복자음,복모음 대응표 입니다. byte middle2[7][3]={ {13,3,14},{13,4,15},{13,29,18},{20,7,21}, {20,10,22},{20,29,23},{27,29,28} }; byte last2[11][3]={ {2,11,4},{5,14,6},{5,20,7},{9,2,10},{9,8,11}, {9,9,12},{9,11,13},{9,18,14},{9,19,15}, {9,20,16},{19,11,20}}; 위의 것이 복모음 대응표로서 모음번호 13 과 4 가 합치면 14 가 됨을 나타냅니다.... 그다음의 표가 좀 복잡한데.... 복종성의 경우인데.. 복종성은 종성이 이미 있을때 자음을 입력하면 체크하는 부분이므로 제일 앞의 2,11,4 는 종성번호 2+초성번호 11 은 종성번호 4 임을 나타냅니다. 그위의 5,14,6 은 종성 5+초성 14=종성 6 임을 나타 냅니다. 마지막으로 한가지 더 짚고 넘어갈것은 ㅤㅤㅚㅀ습 나타낸다고 보면 됩니다... #define EMPTY 1 등으로 해두면 if (han.c.firc==EMPTY) 짠. 하고 지정이 가능하겠죠 ? 그럼 이제 실제로 남은 부분은 의외로 간단합니다..하도 사전준비 가 충실했기 때문이랄까요 ? 자.. 우선 자음을 입력했을때의 상황을 살펴보기로 합시다. 한글의 특성상 초성이 없고 종성만 있는 상황은 없읍니다.왜냐하면 한자만 입력하면 초성으로 분류되기 때문입니다.. 곧 초성과 중성이 비었으면 글자가 아직 하나도 입력되지 않은 상황이죠. * 자음의 오토마타 1. 초성과 중성이 비었는가를 확인(모두 비었는가 ?) 비었으면 초성에자음코드를 넣는다.(키보드-자모 표에서의 리턴값은 초성의 코드를 가지고 있으므로 그냥 대입합니다. 2. 그게 아니면 초성이 있고 중성이 없다면 새로운 글자이므로 (초성은 자음이 두개 겹쳐서 입력하는 경우는 없읍니다. 까,따 와 같은건 한번에 입력하죠 ? ) 현 글자를 완성시키고 다음 글자의 초성에 대입한다. 3. 중성이 비어 있지 않은 경우이므로 이때 종성이 비어 있으면 입력된 코드(초성의 코드)를 초성-종성 표에 의해 종성으로 변환하여 종성코드(hanin.c.lasc) 에 넣습니다. 그런데 이 초성-종섯 표로 찾아본 결과 표에 없는 글자(초성 에는 있으나 종성에는 없는 글자,짜 같은거) 라면 현글자를 마치고 다음글자의 초성에 대입합니다. 종성이 비지 않은 경우라면 복종성이 될 가능성이 있으므로 그 종성과 현재의 초성 코드를 자료로 해서 복종성표에서 찾 아봅니다.. 만일 있으면 현재의 종성을 그 복종성으로 바꾸어 넣고 없으면 현재의 종성을 그냥 놔두고 현글자를 마치고 다음 글자의 초성에 대입합니다. 말이 복잡하죠 ? 하지만 프로그램은 그리 복잡하지는 않습니다. void ja_process(void) { int temp; if (hanin.c.firc==EMPTY && hanin.c.midc==EMPTY) hanin.c.firc=h_code; else if (hanin.c.firc!=EMPTY && hanin.c.midc==EMPTY){ complete(); hanin.c.firc=h_code; } else if (hanin.c.midc!=EMPTY) if (hanin.c.lasc==EMPTY) if ((temp=last_by_first(h_code))!=NO_CODE) hanin.c.lasc=temp; else { complete(); hanin.c.firc=h_code; } else if ((temp=last2_find())==NO_CODE){ complete(); hanin.c.firc=h_code; } else hanin.c.lasc=temp; } 위에서 last_by_first() 함수는 초성->종성의 변환을 합니다. last2_find() 함수는 복종성의 체크를 해서 없으면 NO_CODE,있으면 복종성 캡ㅤㅤㅤㄴㅤㅤㅗㅈ 리턴합니다.complete() 는 현글자를 완성합니다. 사실 뒷부분도 한번에 올릴려고 했는데.... 글쎄 황당하게도 500 라인이 넘으면서 더이상 안된다고 해서..눈물을 머금고 두개로 나누었읍니다... 500 라인이 한계예요.... 알아두세요.... 다음은 모음의 오토마타부터 시작입니다. #81 한우진 (hwjketel) 한글 입력 오토마타의 설명 2 01/23 10:50 286 line 안녕하세요 허르미 입니다... 한글 입력 오토마타 2 번째 시간입니다..모음의 오토마타 부터 시작합니다. * 모음의 오토마타.. 조금 더 복잡합니다. 우선 입력된 코드에서 100 을 빼야 겠죠 ? 아까 키보드-자모 표에서 100 이 더해진 걸 기억하시죠 ? 1. 종성이 차있다면 혹시 그 종성이 다음 글자의 초성을 이룰수 있는가를 검사합니다.. 그러니까 한 에서 아 를 치면 하나 가 되쟎아요 ? 이처럼 받침 니은이 다음글자의 초성으로 따라올라 가는 처리를 해줍니다.. 즉, 종성이 차있다면 그 종성이 복종성인지를 검사합니다. 위에서 상호참조라는 말을 했는데 이 경우가 복종성이 어떤 종성+초성으로 갈라지는 지를 찾습니 다 ... 복종성임이 판별되면 위처럼 갈라서 종성부분은 현 글자의 종성으로 넣고 현글자를 완성한다음 초성부분을 다음글 자의 초성으로 넣고 지금 입력된 모음 코드를 중성으로 넣습 니다. 복종성이 아니라면. 그냥 현글자의 종성을 초성으로 변환하여 놓고 현 종성을 비운다음 현글자를 완성시키고 다음글자의 초성에 아까 변환한 값을 넣고 중성에 입력된 모음 코드를 넣습니다. 2. 아니면 중성이 비어있는 경우라면 중성에 그냥 대입합니다. 3. 중성이 차있는 경우 복중성인지를 체크해야 합니다. 복중성표 에 의해서 지금 있는 중성과 입력된 중성을 합쳐서 복중성이 된다면 복중성으로 현재의 중성을 바꾸어 넣고 아니면 현글자는 완성시키고 다음글자의 중성으로 넣습니다. 1 번이 좀 복잡하죠 ? 프로그램을 봅시다. void mo_process(void) { int temp; h_code-=MID_OFF; if (hanin.c.lasc!=EMPTY) if ((temp=last2_find_mo())==NO_CODE) { temp=first_by_last(hanin.c.lasc); hanin.c.midc=h_code; } else if (hanin.c.midc==EMPTY) hanin.c.midc=h_code; else { if ((temp=middle2_find())==NO_CODE) { complete(); hanin.c.midc=h_code; } else hanin.c.midc=temp; } } 위에서 first_by_last() 는 종성->초성 변환값을 리턴하며.... last2_find_mo(); 는 복종성표에서 복종성->종성+초성으로 해주는 함수입니다. 종성은 바꾸어 넣고 초성값을 리턴하니다. 곧 앞에서 사용된 last2_find() 와는 역참조입니다. middle2_find 는 복모음을 체크합니다. 이런 참조 함수들을 나열해 보겠읍니다.. int last_by_first(int code) { int i; for (i=0;i<16;i++) if (first_last[i][0]==code) return first_last[i][1]; return NO_CODE; } int first_by_last(int code) { int i; for (i=0;i<16;i++) if (first_last[i][1]==code) return first_last[i][0]; return NO_CODE; } int last2_find(void) { int i; for (i=0;i<11;i++) if (last2[i][0]==hanin.c.lasc && last2[i][1]==h_code) return (last2[i][2]); return NO_CODE; } int middle2_find(void) { int i; for (i=0;i<7;i++) if (middle2[i][0]==hanin.c.midc && middle2[i][1]==h_cod e) return (middle2[i][2]); returnNO_CODE; } int last2_find_mo(void) { int i; for (i=0;i<11;i++) if (last2[i][2]==hanin.c.lasc) { hanin.c.lasc=last2[i][0]; return(last2[i][1]); } return NO_CODE; } int middle2_find_mo(void) { int i; for (i=0;i<7;i++) if (middle2[i][2]==hanin.c.midc) { hanin.c.midc=middle2[i][0]; return(middle2[i][1]); } return NO_CODE; } 위의 함수들중 지금까지 쓰이지 않은것이 있는데 이것이 middle2_find_mo(); 입니다..이름에서 짐작하셨겠지만 middle2_find(); 의 역참조입니다. 복모음을 두 모음으로 분리해 냅니다. 앞의 모음은 바꾸어 넣고 뒤모음은 리턴합니다. 이는 BS 키이의 구현에서 사용됩니다. 복모음은 BS 키이에 의해 자모별로 지워져야 하니까요.. DEL 키이는 한자씩 지우 는 반면에 BS 키이는 현글자가 완성되지 않았다면 자모단위 로 지워줍니다. 중요한 부분은 거의 다 한셈입니다. 복잡한 오토마타도 (사실 말이 오토마타지 제가 머리싸매고 여러번 수정끝에 만든것이므로 버그가 있을지 모르나 꽤 오랜 동안의 테스트로 별로 버그가 없을것이라 생각함..공개용 오토마타를 구하지 못한 관계로해서 돌굴리느라 애먹었음.) 되었고. BS,DEL 키이와 같은 부분의 구현은 간단히 살펴보기로 합시다. 우선 중요한 함수 하나를 소개하겠읍니다... 현재의 커서위치 의 앞에 있는 글자가 영어인지 한글인지는 맨앞에서 부터 세봐야지 알수 있읍니다.. 바로 앞의것에 0x80 을 AND 하는 방법으로는 알수 없읍니다.. 더구나 isascii 같은 것으로는 더더욱..! int is_hangul(void) { int ret_mode,i; for (i=0;i