삽질하는플머

친구 부탁으로 진행했던 3D 맛보기 세미나.

탐구생활/Delphi
3D란 무엇인가. 행렬과 벡터. 예제 살펴보기라는 세가지 단락으로 한시간 정도 정신없이 떠들었음. 
환경은 델파이와 OpenGL. 

다른 인간 부탁이었으면  생깠을텐데 항상 고마운 친구라... 사실 이런건 준비하는 사람에게 더 도움이 되는 일이기도 하고... 
강사자질도 없는 버벅 버벅 대왕이라 오히려 악의 구렁텅이로 이끈 건 아닌지 걱정... 
진행했던 세미나 중 유일하게 한시간을 넘김. (최단기록은 논문발표... 7분...)

전반부는 말로 때우고, 후반부는 예제의 주석을 풀어가며 기능을 설명하는 방식으로  진행했음. 


발표에 사용한 ppt 파일과 델파이로 만든 예제를 묶어서 올려 둠. 
  

2011.4.23.세미나.zip




유니코드 커맨드 라인 실행하고 결과값 받아오기

탐구생활/Delphi
소위 이야기하는 도스커맨드 실행하고 결과 받아오기. 김성동님의 코드를 살짝 바꿔 사용. 
커맨드라인 파라미터를 유니코드로 넘길 수 있게 하고, 중간 중간에 이루어지는 출력을 받아서 처리할 수 있는 콜백버전을 새로 만듦. 

다국어 INI를 읽어 UTF-8 로 뿌려주는 간단한  유틸리티를 만들었는데, 이 유틸리티를 호출하려면 INI 파일의 이름을 유니코드 파라미터로 넘겨야 하기 때문에 필요해짐. 


// 유니코드 커맨드라인 실행하고 출력값 얻어오자. .
// 주의! 콘솔로 출력되는 문자열은 안시문자열이다. 
// GetConsoleOutputCP(), SetConsoleOutputCP() API 참조. 

type
  TRunCmdCallback = procedure (const aRetStr: AnsiString; aParam: Pointer);

// 실행결과 문자열을 RunCmdCallback으로 조금씩 받아 조립할 때 사용.
// 콜백의 호출 주기는 상수 BUFFER_SIZE 의 값과 WaitForSingleObject() 의 두번째 인자인 시간값으로 결정된다. 
function RunWideCmd(Command: WideString; RunCmdCallback: TRunCmdCallback; Param: Pointer = nil): DWORD; overload;
const
  BUFFER_SIZE = 512; // 버퍼 크기가 작으면 RunCmdCallback이 좀 더 자주 일어남
var
  hReadPipe : THandle;
  hWritePipe : THandle;
  SI : TStartUpInfo;
  PI : TProcessInformation;
  SA : TSecurityAttributes;
  SD : TSecurityDescriptor;
  BytesRead : DWORD;
  Avail, wrResult : DWORD;
  RetStr: AnsiString;
begin
  { Dos Application }
  if Win32Platform = VER_PLATFORM_WIN32_NT then
  begin
    InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(@SD, True, nil, False);
    SA.nLength := SizeOf(SA);
    SA.lpSecurityDescriptor := @SD;
    SA.bInheritHandle := True;
    CreatePipe(hReadPipe, hWritePipe, @SA, BUFFER_SIZE);
  end else
    CreatePipe(hReadPipe, hWritePipe, nil, BUFFER_SIZE);

  try
    { STARTUPINFO를 설정한다. }
    FillChar(SI, SizeOf(SI), 0);
    SI.cb := SizeOf(TStartUpInfo);
    SI.wShowWindow := SW_HIDE;
    SI.dwFlags := STARTF_USESHOWWINDOW;
    { STARTF_USESTDHANDLES 를 빼면 PIPE로 입출력이 Redirect 되지 않는다. }
    SI.dwFlags := SI.dwFlags or STARTF_USESTDHANDLES;
    SI.hStdOutput := hWritePipe;
    SI.hStdError  := hWritePipe;
    { Process를 생성한다. }
    if CreateProcessW(nil, PWChar(Command), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, SI, PI) then
    begin
      Result := 0;
      while Result = 0 do
      begin
      // 대기시간이 길면 반응이 느리다. 짧으면 콜백이 그만큼 자주 일어남. 적당히 조절해 사용. 
        wrResult := WaitForSingleObject(PI.hProcess, 5); 
        { Pipe에 출력된 내용이 있는 지 조사한다. }
        if PeekNamedPipe(hReadPipe, nil, 0, nil, @Avail, nil) then
        begin
          if Avail > 0 then
          begin
            { Redirect된 화면 내용을 얻어낸다. }
            SetLength(RetStr, Integer(Avail));
            ReadFile(hReadPipe, PAnsiChar(RetStr)[0], Avail, BytesRead, nil);

            if Assigned(RunCmdCallback) then RunCmdCallback(RetStr, Param);
          end;
        end;
        { WAIT_TIMEOUT이 아니면 루프를 마친다. }
        if wrResult <> WAIT_TIMEOUT then Result := 1;
      end;
      { 실행이 끝났다.. }
      GetExitCodeProcess(PI.hProcess, Result);
      CloseHandle(PI.hProcess);
      CloseHandle(PI.hThread);
    end;
  finally
    CloseHandle(hReadPipe);
    CloseHandle(hWritePipe);
  end;
end;

// 실행결과 문자열 전체를 한꺼번에 리턴받을 때 사용.
function RunWideCmd(Command : WideString; var RetStr: AnsiString): DWORD; overload;
const
  BUFFER_SIZE = 512;
var
  hReadPipe : THandle;
  hWritePipe : THandle;
  SI : TStartUpInfo;
  PI : TProcessInformation;
  SA : TSecurityAttributes;
  SD : TSecurityDescriptor;
  BytesRead : DWORD;
  Avail, wrResult : DWORD;

  LastSize: Integer;
begin
  { Dos Application }
  if Win32Platform = VER_PLATFORM_WIN32_NT then
  begin
    InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(@SD, True, nil, False);
    SA.nLength := SizeOf(SA);
    SA.lpSecurityDescriptor := @SD;
    SA.bInheritHandle := True;
    CreatePipe(hReadPipe, hWritePipe, @SA, BUFFER_SIZE);
  end else
    CreatePipe(hReadPipe, hWritePipe, nil, BUFFER_SIZE);

  try
    { STARTUPINFO를 설정한다. }
    FillChar(SI, SizeOf(SI), 0);
    SI.cb := SizeOf(TStartUpInfo);
    SI.wShowWindow := SW_HIDE;
    SI.dwFlags := STARTF_USESHOWWINDOW;
    { STARTF_USESTDHANDLES 를 빼면 PIPE로 입출력이 Redirect 되지 않는다. }
    SI.dwFlags := SI.dwFlags or STARTF_USESTDHANDLES;
    SI.hStdOutput := hWritePipe;
    SI.hStdError  := hWritePipe;
    { Process를 생성한다. }
    if CreateProcessW(nil, PWideChar(Command), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, SI, PI) then
    begin
      Result := 0;
      while Result = 0 do
      begin
        wrResult := WaitForSingleObject(PI.hProcess, 5);
        { Pipe에 출력된 내용이 있는 지 조사한다. }
        if PeekNamedPipe(hReadPipe, nil, 0, nil, @Avail, nil) then
        begin
          if Avail > 0 then
          begin
            { Redirect된 화면 내용을 얻어낸다. }
            LastSize := Length(RetStr);
            SetLength(RetStr, LastSize + Integer(Avail));
            ReadFile(hReadPipe, PChar(RetStr)[LastSize], Avail, BytesRead, nil);
          end;
        end;
        { WAIT_TIMEOUT이 아니면 루프를 마친다. }
        if wrResult <> WAIT_TIMEOUT then Result := 1;
      end;
      { 실행이 끝났다.. }
      GetExitCodeProcess(PI.hProcess, Result);
      CloseHandle(PI.hProcess);
      CloseHandle(PI.hThread);
    end;
  finally
    CloseHandle(hReadPipe);
    CloseHandle(hWritePipe);
  end;
end;


두번째 함수는 사용법 간단. 대충 다음과 같이. 

procedure TForm1.Button5Click(Sender: TObject);
var
  RetStr: AnsiString;
begin
  RunWideCmd('cmd /c dir c:\', RetStr);
  ListBox1.Items.Text := RetStr;
  ListBox1.ItemIndex := ListBox1.Count -1;
end;


첫번째 함수는 실제 콘솔창처럼 변화하는 상태를 표현할 때 써먹기 위해 만들었는데 사용은 약간 귀찮다. 
(콜백의 호출 주기는 RunWideCmd 함수 내의 상수 BUFFER_SIZE 의 값과 WaitForSingleObject() 로 전달되는 시간값으로 결정된다.) 

var
  ugRetStr: AnsiString;

procedure RunCmdCallback (const aRetStr: AnsiString; aParam: Pointer);
begin
  ugRetStr := ugRetStr + aRetStr;

  TListBox(aParam).Items.BeginUpdate;
  TListBox(aParam).Items.Text := ugRetStr;
  TListBox(aParam).ItemIndex := TListBox(aParam).Count -1;
  TListBox(aParam).Items.EndUpdate;
  Application.ProcessMessages; 
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  ugRetStr := '';
  RunWideCmd('cmd /c dir c:\', RunCmdCallback, ListBox1);
end;


덤으로... 이렇게 호출당한 입장에서 유니코드 파라미터를 참고하는 방법은 다음과 같다. 

function GetWideParamStr(P: PWideChar; var Param: WideString): PWideChar;
var
  i, Len: Integer;
  Start, S, Q: PWideChar;
begin
  while True do
  begin
    while (P[0] <> #0) and (P[0] <= ' ') do
      P := CharNextW(P);
    if (P[0] = '"') and (P[1] = '"') then Inc(P, 2) else Break;
  end;
  Len := 0;
  Start := P;
  while P[0] > ' ' do
  begin
    if P[0] = '"' then
    begin
      P := CharNextW(P);
      while (P[0] <> #0) and (P[0] <> '"') do
      begin
        Q := CharNextW(P);
        Inc(Len, Q - P);
        P := Q;
      end;
      if P[0] <> #0 then
        P := CharNextW(P);
    end else
    begin
      Q := CharNextW(P);
      Inc(Len, Q - P);
      P := Q;
    end;
  end;

  SetLength(Param, Len);

  P := Start;
  S := Pointer(Param);
  i := 0;
  while P[0] > ' ' do
  begin
    if P[0] = '"' then
    begin
      P := CharNextW(P);
      while (P[0] <> #0) and (P[0] <> '"') do
      begin
        Q := CharNextW(P);
        while P < Q do
        begin
          S[i] := P^;
          Inc(P);
          Inc(i);
        end;
      end;
      if P[0] <> #0 then P := CharNextW(P);
    end else
    begin
      Q := CharNextW(P);
      while P < Q do
      begin
        S[i] := P^;
        Inc(P);
        Inc(i);
      end;
    end;
  end;

  Result := P;
end;

function WideParamStr(Index: Integer): WideString;
var
  P: PWideChar;
begin
  Result := '';
  if Index = 0 then
  begin
    SetLength(Result, MAX_PATH);
    SetLength(Result, GetModuleFileNameW(0, PWideChar(Result), MAX_PATH));
  end else
  begin
    P := GetCommandLineW;
    while True do
    begin
      P := GetWideParamStr(P, Result);
      if (Index = 0) or (Result = '') then Break;
      Dec(Index);
    end;
  end;
end;


위에 정의된 WideParamStr() 함수를 ParamStr() 함수 대신에 사용하면 됨. 


숫자를 한글로 바꾸기

탐구생활/Delphi

간단할 것 같았는데 만단위 절삭, "일"을 표시하거나 말아야 하는 경우 등  생각해야 할 부분이 꽤 많다. 
사실 델마당 강좌란에 이미 병규형님이 만들어두신 숫자 -> 한글 함수가 있지만, "일"을 생략해야 하는 경우의 처리가 없어서 이번 기회에 잘 버무려놓기로 한다. 

문제는 귀차니즘... 잠시 고민하다 최종욱님 블로그에서 자바로 된 코드를 걷어다 델파이로 바꾸어보았다. 



(*
  임의의 숫자를 받아 한국어 숫자 표기 기법에 따라 변환한 문자열을 반환

  최종욱님의 자바코드를 델파이로 변환.
  http://link.egloos.com/4165494

  파라미터
    Number    : 한국어로 바꿀 숫자
    DecimalSp : 만단위 단위로 공백 삽입 여부. 
    Official  : 이 값이 True 면 '일'을 생략하지 않는다.
*)

function IntToKorean(Number: Int64;
  DecimalSp: Boolean = false;
  Official: Boolean = false): String;
const
  NumberStr : Array [0..7] of String = (
    '이', '삼', '사', '오', '육', '칠', '팔', '구'
  );
  LevelStr : Array [0..2] of String = (
    '십', '백', '천'
  );
  DecimalStr : Array [0..4] of String = (
    '만', '억', '조', '경', '해'
  );

var
  Digits: String;
  Len: Integer;
  SubCount: Boolean;

  Base: Integer;     // 현재 다루고 있는 자릿수. 
  BaseMod4: Integer; // 만단위내 일십백천 단위.
  Digit: Integer;

  i: Integer; 
begin
  if Number = 0 then
    Result := '영'
  else
  if (Number < 0) then
    Result := '마이너스 ' + IntToKorean(-Number, Official)
  else
  begin
    Digits := IntToStr(Number);
    Len := Length(Digits);
    SubCount := false;

    for i:= 0 to Len-1 do
    begin
      Base := Len -i -1;
      BaseMod4 := Base mod 4;
      Digit := Ord(Digits[i+1]) - Ord('0');

      if Digit <> 0 then
      begin
        SubCount := true;

        if Digit = 1 then
        begin
          if Official or (BaseMod4 = 0) then
            Result := Result + '일';
        end else
          Result := Result + NumberStr[Digit-2];

        if BaseMod4 > 0 then
          Result := Result + LevelStr[BaseMod4-1];
      end;


      if BaseMod4 = 0 then
      begin
        if SubCount and (Base > 0) then
        begin
          Result := Result + DecimalStr[Base div 4 - 1];

          if DecimalSp then
            Result := Result + ' ';
        end;
      end;
    end;
  end;
end;


숫자값 123523121821 을 변환할 경우 

IntToKorean(123523121821);
  결과 : 천이백삼십오억이천삼백십이만천팔백이십일

IntToKorean(123523121821, true);
  결과 : 천이백삼십오억 이천삼백십이만 천팔백이십일

IntToKorean(123523121821, true, true);
  결과 : 일천이백삼십오억 이천삼백일십이만 일천팔백이십일


다국어를 지원하는 TWideIniFile

탐구생활/Delphi
내가 만들고자 하는 물건은 이런 것.



어떤 아이디어가 떠오를 때, 동시에 같은 생각을 하는 사람이 100명이고 그 중 10명은 이미 만들고 있고, 한명은 벌써 완성했다는 옛 이야기가 있다. 그것이 진리임을 다시 깨닫게 되는 링크.

http://simplyplay.googlecode.com/svn-history/r59/trunk/WideIniFiles.pas

외부 저장만 UTF-8 로 바꿔주면 아주아주 쓸만할 듯.

하지만... 이미 만들었는걸... 앞으로는 찾아보고 만들자. 
윈도에서 저장된 UTF-8 문서의 경우 앞부분에 BOM이 따라붙으므로 이 부분은 건너뛰도록 하는 코드도 추가. 
저장시에도 BOM을 붙여준다. 



LVCL 고쳐쓰기.

탐구생활/Delphi
일단 홈페이지는 여기. 라이센스는 MPL.
http://bouchez.info/lvcl.html

이름 그대로 가벼운 VCL 이다. 사용법도 아주 간단해서, 작업중인 소스와 함께 두어도 되고 라이브러리 경로 중 "맨 위에" 추가해도 된다.
폼 하나를 컴파일해보면 달랑 45kb로 떨어진다. 라자루스에서 애용하던 KOL의 경우 터보델파이에서 컴포넌트 설치가 되지 않으므로 쉽지 않은 길을 가야 하는 반면, 기존 유니트들의 참조를 바꾸는 방식이므로 터보델파이에서도 무리없이 사용할 수 있다.  

현재 TButton, TCheckBox, TEdit, TLabel, TMemo 다섯가지 컨트롤이 구현되어 있다. 때문에 VCL 중 달랑 폼 하나만 사용하는 나같은 게임 프로그래머에게는 아주 멋진 물건이다.

일단 손에 닿는 몇가지 개선점만 고쳐보았다.

1. 폼을 비롯한 윈도 컨트롤의 크기 조절시 배경 갱신 문제 수정.
2. 윈도 컨트롤 외에는 마우스 이벤트가 발생하지 않는 문제 수정. 덤으로 우측 마우스 메시지도 처리.
3. TScrollBar 컨트롤 추가.
4. ClientWidth / ClientHeight 기능 추가.
5. 프로퍼티 에디터에서 설정한 Visible 속성이 제대로 반영되도록 수정.
6. TEdit / TMemo 에서 한글로 된 ImeName 프로퍼티를 읽을 때 생기는 오류 수정.

이 LVCL을 기반으로 한 간단한 테스트 프로그램을 배포할 예정이다. 때문에 수정한 LVCL의 소스코드를 MPL에 따라 여기에 배포한다.
(원저자에게도 보내야 하는군. 우이씨~ 영어 짧은디~~)




최신버전의 LVCL은 dkw 프로젝트에서 관리한다. 
http://dev.naver.com/projects/dkw/
svn checkout --username anonsvn https://dev.naver.com/svn/dkw/trunk/LIB/LVCL