저작권에 대한 공지
이 문서에 포함된 모든 글과 소스 코드는 작자인 저 김성동의 지적 재산이지만 무단으로 배포하거나 책이나 강의 교재등을 위한 복제 등 저작권을 저해하는 행위를 제외하고는 자유롭게 사용할 수 있습니다.
또한 이 페이지에 대한 링크는 자유롭게 할 수 있으나 전문을 전제하는 일은 허용하지 않습니다.


2.5. 데이터 연동 콤포넌트(Data-Aware Component) 개발

2.5.1. 데이터 연동(Data Aware)
이전 절까지 콤포넌트 개발에 필요한 제반 사항에 대해서 거의 알아 보았으며 간단한 콤포넌트를 개발해 보았다. 하지만 마지막으로 한가지 더 공부해야할 항목이 하나 남아 있다. 델파이 콤포넌트 팔레트의 Data Controls 페이지를 보면 TDBGrid, TDBText, TDBEdit, TDBMemo 등 여러 가지 콤포넌트가 있다. 이들 콤포넌트가 다른 페이지에 있는 콤포넌트들과 다른 점은 바로 콤포넌트 자체적으로 데이터 소스와 연결이 되고 부가적인 코드를 작성해 주지 않아도 데이터베이스의 현재 레코드의 내용을 자동으로 반영해 주고 또한 수정도 가능하다는 것이다. 이런 콤포넌트들을 데이터 연동 콤포넌트라 하는데 이번 절에서는 개발한 콤포넌트가 데이터 연동 기능을 지원하려면 어떻게 해야 하는지를 알아 보겠다.
데이터 연동 기능은 두가지 형태로 구분할 수 있는데 데이터 열람(Data Browsing)과 데이터 수정(Data Editing)이다. 데이터 열람은 단순히 현재 데이터베이스의 내용을 콤포넌트에 반영해 주기만 하면 되고 데이터 수정은 데이터 열람 기능 뿐만 아니라 콤포넌트를 조작하므로서 데이터베이스의 현재 레코드의 내용을 수정하거나 새로운 레코드를 추가할 수 있는 형태이다. 또한 데이터 연동은 TDBEdit처럼 그 범위가 단일 필드에만 국한될 수도 있고 TDBGrid처럼 DataSet 전체에 연결될 수도 있다.
데이터 연동 콤포넌트를 개발할 때는 이전 절에서처럼 콤포넌트를 어떤 특정 부모 클래스에서 상속 받아서 작성해야 하는 것이 아니다. 대신에 데이터 연동 콤포넌트는 기존에 개발한 콤포넌트에 데이터 연동 기능을 추가해 주는 과정이라고 생각하면 쉽다. 즉 콤포넌트 내부에 데이터 링크를 생성해주고 데이터 연동을 위한 몇가지 프로퍼티를 추가해주면 된다.
2.5.2. 데이터 링크(Data Link)
콤포넌트와 데이터베이스는 데이터 링크를 통해서 연결된다. 콤포넌트의 데이터 연동 형태에 상관 없이 데이터 연동 콤포넌트는 DataSet을 조작하기 위해 DataSource 라는 프로퍼티를 통해 데이터 링크를 생성해야 한다. 데이터 링크는 TDataLink 클래스나 그 자손 클래스(TFieldDataLink, TGridDataLink 등)을 통해서 생성된다. 만약 콤포넌트가 단일 필드를 조작한다면 TFieldDataLink 클래스를 사용하며 다중 필드와 다중 레코드를 조작한다면 TDataLink의 자손 클래스를 새로 만들어서 사용하게 된다.
아래에 TDataLink와 TFieldDataLink의 중요한 프로퍼티와 메소드, 그리고 이벤트들에 대해 나타내었다.


표 2-1 TDataLink의 프로퍼티
프로퍼티설명
ActiveDataSet이 활성화 상태인지 아닌지를 설정한다.
ActiveRecordDataSet 내부 버퍼가 가지고 있는 레코드에서 몇번째 레코드가 현재 레코드인지를 나타낸다. ActiveRecord는 전체 레코드 개수에서의 순번이 아니고 내부 버퍼에 있는 래코드에서의 순번임을 주의해야 한다.
BOF커서가 DataSet의 첫번째 레코드에 있음을 나타낸다.
BufferCount내부 버퍼에 읽어들일 레코드의 개수를 나타낸다.
DataSet데이터 링크가 연결된 DataSet을 나타낸다.
DataSource데이터 링크의 소유주(데이터 연동 콤포넌트)가 DataSet을 사용할 때 사용하는 DataSource 콤포넌트를 지정한다.
EditingDataSet이 편집 모드임을 나타내다.
EOF커서가 DataSet의 마지막 레코드에 있음을 나타낸다.
ReadOnlyDataSet을 수정할 수 있는지를 나타낸다.
RecordCount내부 버퍼에 실제로 몇 개의 레코드가 있는지를 나타낸다. 보통 BufferCount와 같은 값이지만 TDBGrid처럼 BufferCount보다 작을 경우도 있다.




표 2-2 TDataLink의 메소드
메소드 설명
ActiveChangedActive 프로퍼티가 변경될 때 마다 호출된다.
DataSetChangedDataSet의 내용이 변경될 때 마다 호출된다. DataSet의 내용이 수정되거나 새로운 레코드가 추가되거나 레코드가 삭제될 때 호출된다. 실제로 DataSetChanged는 RecordChanged를 호출한다.
EditDataSet을 편집 모드로 변경한다.
EditingChangedEditing 프로퍼티가 변경될 때 마다 호출된다.
RecordChanged현재 레코드의 필드의 내용이 변경될 때 마다 호출된다. 필드의 내용이 포스트된 후에 발생한다.
UpdateRecord콤포넌트가 필요한 경우에 호출할 수 있으며 콤포넌트의 상태를 DataSet에 반영한다. 실제로 UpdateData를 호출한다.




표 2-3 TFieldDataLink의 프로퍼티
프로퍼티설명
CanModify필드를 편집할 수 있는지를 나타낸다.
ControlField 객체의 FocusControl 프로퍼티와 데이터 연동 콤포넌트를 연결하기 위해서 TFieldDataLink 객체를 사용하는 데이터 연동 콤포넌트를 설정한다.
Field연결된 필드 객체를 나타낸다.
FieldName연결된 필드의 이름을 나타내다.




표 2-4 TFieldDataLink의 이벤트
이벤트설명
OnActiveChangeActive 프로퍼티가 변경될 때 발생한다.
OnDataChange필드의 값이 변경될 대 발생한다.
OnEditingChangeDataSet의 모드가 변경될 때 발생한다.
OnUpdateData필드의 값이 콤포넌트의 값으로 업데이트될 때 발생한다.


2.5.3. 데이터 열람(Data Browsing) 콤포넌트
간단하게 인맥 테이블의 내용을 명함처럼 보여주는 데이터 열람 전용 콤포넌트인 TdpbNameCard를 만들어 보자. 아래에 완성된 TdpbNameCard 콤포넌트가 동작하는 화면과 소스 코드를 나타내었다.

그림 2-13 TdpbNameCard 동작 예제




리스트 2.5 dpbNameCard.pas
unit dpbNameCard;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, db, dbTables, ExtCtrls;

type
  TdpbNameCard = Class;

  { 데이터 링크 클래스 선언 }
  TdpbNameCardDataLink = Class(TDataLink)
  private
    FCard: TdpbNameCard;
  protected
    procedure DataSetChanged; override;
    procedure DataSetScrolled(Distance : Integer); override;
    procedure ActiveChanged; override;
  public
    constructor Create(ACard: TdpbNameCard);
    destructor Destroy; override;
  End;

  TdpbNameCard = class(TCustomControl)
  private
    { Private declarations }
    FBorderStyle : TBorderStyle;
    FDataLink : TdpbNameCardDataLink;
    FStartRecord : Integer;
    FViewConfiguration : TStrings;
    FColCount : Integer;
    FRowCount : Integer;
    FNameFont : TFont;
    FNameColor : TColor;
    FCardWidth : Integer;
    FCardHeight : Integer;
    FCardColor : TColor;
    FCardBorderColor : TColor;

    procedure SetBorderStyle(Value : TBorderStyle);
    function GetDataSource: TDataSource;
    procedure SetDataSource(Value: TDataSource);
    procedure SetViewConfiguration(Value : TStrings);
    procedure Changed(Sender : TObject);
    procedure SetCardWidth(Value : Integer);
    procedure SetCardHeight(Value : Integer);
    procedure SetNameFont(Value : TFont);
    procedure SetCardColor(Value : TColor);
    procedure SetCardBorderColor(Value : TColor);
    procedure SetNameColor(Value : TColor);
    procedure SetVertScrollRange(Const AMin, AMax : Integer);
    procedure SetVertScrollPos(Const APos : Integer);
    procedure AdjustColRow;
    procedure DoVScroll(ScrollCode : TScrollCode);

    procedure DataChanged;
    procedure ActiveChanged;
  protected
    { Protected declarations }
    procedure CreateWnd; override;
    procedure CreateParams(var Params: TCreateParams); Override;
    procedure CMCtl3DChanged(var Message: TMessage); message CM_CTL3DCHANGED;
    procedure WMSetFocus(var Message: TWMSetFocus); message WM_SETFOCUS;
    procedure WMKillFocus(var Message: TWMKillFocus); message WM_KILLFOCUS;
    procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
    procedure WMEraseBkgnd(var Message: TMessage); message WM_ERASEBKGND;
    procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState;
              X, Y: Integer); override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;

    procedure UpdateVertScrollBar;
    function  GetCellRect(const ACol, ARow : Integer) : TRect;
    function  VisibleRows : Integer;
  public
    { Public declarations }
    Constructor Create(AOwner: TComponent); Override;
    Destructor Destroy; Override;
    procedure Paint; override;
    procedure Loaded; override;
  published
    { Published declarations }
    Property Align;
    Property BorderStyle : TBorderStyle
             Read FBorderStyle
             Write SetBorderStyle
             Default bsSingle;

    property CardBorderColor : TColor
             read FCardBorderColor
             write SetCardBorderColor
             default clBtnFace;
    property CardColor : TColor
             read FCardColor
             write SetCardColor
             default clWindow;

    property CardHeight : Integer
             read FCardHeight
             write SetCardHeight
             default 90;

    property CardWidth : Integer
             read FCardWidth
             write SetCardWidth
             default 60;

    Property Color;
    property Constraints;
    Property Ctl3D;

    property DataSource: TDataSource
             read GetDataSource
             write SetDataSource;

    Property Font;
    property Hint;

    property NameColor : TColor
             read FNameColor
             write SetNameColor
             default clBtnFace;

    property NameFont : TFont
             read FNameFont
             write SetNameFont;

    Property ParentColor;
    property ParentCtl3D;
    Property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property ViewConfiguration : TStrings
             read FViewConfiguration
             write SetViewConfiguration;

    property OnClick;
    property OnDblClick;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('DPB', [TdpbNameCard]);
end;

function GetWord(AValue : string; AIndex : Integer; ADelimiter : char) : string;
var
  nIndex, DelNum : integer;
begin
  Result := '';
  DelNum := 1;
  for nIndex := 1 to Length(AValue) do
  begin
    if AValue[nIndex] = ADelimiter then
      Inc(DelNum)
    else
    begin
      if DelNum = AIndex then
        Result := Result + AValue[nIndex];
    end;
  end;
  Result := Trim(Result);
end;

function Min( A, B : Integer ) : Integer;
begin
  if A < B then
    Result := A
  else
    Result := B;
end;

{ TdpbNameCardDataLink }

constructor TdpbNameCardDataLink.Create(ACard: TdpbNameCard);
begin
  inherited Create;
  FCard := ACard;
end;

destructor TdpbNameCardDataLink.Destroy;
begin
  FCard := nil;
  inherited Destroy;
end;

procedure TdpbNameCardDataLink.ActiveChanged;
begin
  if FCard <> nil then FCard.ActiveChanged;
end;

procedure TdpbNameCardDataLink.DataSetChanged;
begin
  if FCard <> nil then FCard.DataChanged;
end;

procedure TdpbNameCardDataLink.DataSetScrolled(Distance : Integer);
begin
  if FCard <> nil then FCard.DataChanged;
end;

{ TdpbNameCard }


procedure TdpbNameCard.SetBorderStyle(Value: TBorderStyle);
begin
  if FBorderStyle <> Value then
  begin
    { BorderStyle은 Window의 Style로 설정되기 때문에 BorderStyle이 변경되면
      윈도를 다시 생성하기 위해 RecreateWnd 를 호출한다. }
    FBorderStyle := Value;
    RecreateWnd;
  end;
end;

function TdpbNameCard.GetDataSource: TDataSource;
begin
  Result := FDataLink.DataSource;
end;

procedure TdpbNameCard.SetDataSource(Value: TDataSource);
begin
  if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then
    FDataLink.DataSource := Value;
  if Value <> nil then Value.FreeNotification(Self);
  if not (csLoading in ComponentState) then ActiveChanged;
end;

procedure TdpbNameCard.SetViewConfiguration(Value: TStrings);
begin
  FViewConfiguration.Assign(Value);
  Invalidate;
end;

procedure TdpbNameCard.SetCardWidth(Value: Integer);
begin
  if FCardWidth <> Value then
  begin
    FCardWidth := Value;
    AdjustColRow;
  end;
end;

procedure TdpbNameCard.SetCardHeight(Value: Integer);
begin
  if FCardHeight <> Value then
  begin
    FCardHeight := Value;
    AdjustColRow;
  end;
end;

procedure TdpbNameCard.SetNameFont(Value: TFont);
begin
  FNameFont.Assign(Value);
  Invalidate;
end;

procedure TdpbNameCard.SetCardBorderColor(Value: TColor);
begin
  if FCardBorderColor <> Value then
  begin
    FCardBorderColor := Value;
    Invalidate;
  end;
end;

procedure TdpbNameCard.SetCardColor(Value: TColor);
begin
  if FCardColor <> Value then
  begin
    FCardColor := Value;
    Invalidate;
  end;
end;

procedure TdpbNameCard.SetNameColor(Value: TColor);
begin
  if FNameColor <> Value then
  begin
    FNameColor := Value;
    Invalidate;
  end;
end;

procedure TdpbNameCard.ActiveChanged;
begin
  if HandleAllocated then
  begin
    UpdateVertScrollBar;
    Invalidate;
  end;
end;

procedure TdpbNameCard.DataChanged;
begin
  if HandleAllocated then
  begin
    UpdateVertScrollBar;
    Invalidate;
  end;
end;

procedure TdpbNameCard.CMCtl3DChanged(var Message: TMessage);
begin
  If FBorderStyle = bsSingle Then
    RecreateWnd;
  Inherited;
end;

constructor TdpbNameCard.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  { 캡션을 자동으로 설정할 필요가 없으니까 csSetCaption 플래그를 지운다. }
  ControlStyle := ControlStyle - [csSetCaption];
  Width := 200;
  Height := 150;
  ParentColor := False;
  Color := clWindow;

  FBorderStyle := bsSingle;
  FCardWidth := 90;
  FCardHeight := 60;
  FCardColor := clWindow;
  FCardBorderColor := clBtnFace;
  FNameColor := clBtnFace;
  FStartRecord := 0;


  { 데이터 링크를 생성한다. }
  FDataLink := TdpbNameCardDataLink.Create(Self);

  FViewConfiguration := TStringList.Create;
  TStringList(FViewConfiguration).OnChange := Changed;

  FNameFont := TFont.Create;
  FNameFont.OnChange := Changed;
end;

procedure TdpbNameCard.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do
  begin
    { 수직 스크롤 바를 추가한다. }
    Style := Style or WS_VSCROLL;
    if FBorderStyle = bsSingle then
    begin
      { 콤포넌트의 외곽선을 결정한다. }
      if Ctl3D then
        ExStyle := ExStyle or WS_EX_CLIENTEDGE
      else
        Style := Style or WS_BORDER;
    end;
    WindowClass.style := WindowClass.style and not (CS_HREDRAW or CS_VREDRAW);
  End;
end;

destructor TdpbNameCard.Destroy;
begin
  FNameFont.Free;
  FViewConfiguration.Free;
  FDataLink.Free;
  inherited Destroy;
end;

procedure TdpbNameCard.Paint;
Const
  FieldAlignment : Array[0..2] Of Integer = (DT_LEFT, DT_CENTER, DT_RIGHT);
var
  nIndex : Integer;
  OldActive : Integer;
  StartRow, StopRow : Integer;
  nCol, nRow : Integer;
  szField, szFormat : String;
  CellRect, TmpRect, FieldRect : TRect;
begin
  Canvas.Brush.Color := Self.Color;
  Canvas.FillRect(ClientRect);

  if FDataLink.Active then
  begin
    { 시작 레코드와 마지막 레코드를 구한다. }
    StartRow := FDataLink.ActiveRecord;
    for nIndex := 1 to VisibleRows do
      if StartRow > 0 then
        Dec( StartRow );
    StopRow := Min(FDataLink.RecordCount, StartRow + VisibleRows);
    OldActive := FDataLink.ActiveRecord;
  end
  else
  begin
    OldActive := 0;
    StopRow := 0;
  end;

  StartRow := 0;
  for nRow := 1 To FRowCount do
  begin
    for nCol := 1 To FColCount do
    begin
      { 카드가 그려질 영역을 구한다 .}
      CellRect := GetCellRect(nCol, nRow);

      if FDataLink.Active then
      begin
        if StartRow < StopRow then
        begin
          Canvas.Pen.Color := FCardBorderColor;
          Canvas.Brush.Color := FCardColor;
          Canvas.Rectangle(CellRect.Left, CellRect.Top,
                           CellRect.Right, CellRect.Bottom);

          FDataLink.ActiveRecord := StartRow;
          Inc(StartRow);

          { 이름 영역의 배경을 그린다. }
          Canvas.Font.Assign(FNameFont);
          TmpRect := CellRect;
          TmpRect.Bottom := TmpRect.Top + Canvas.TextHeight('A') + 8;
          if FDataLink.ActiveRecord = OldActive then
          begin
            Canvas.Brush.Color := clHighlight;
            Canvas.Font.Color := clHighlightText;
          end
          else
          begin
            Canvas.Brush.Color := FNameColor;
          end;
          Canvas.FillRect(TmpRect);
          InflateRect(TmpRect, -2, -2);

          { 이름을 출력한다. }
          DrawText(Canvas.Handle,
                   PChar(FDataLink.DataSet.FieldByName('Name').DisplayText + ' ' +
                         FDataLink.DataSet.FieldByName('Position').DisplayText ),
                   -1, TmpRect, DT_LEFT or DT_SINGLELINE or DT_VCENTER);

          {
            그리는 레코드가 현재 레코드이고 입력 포커스를 가지고 있으면
            포커스를 나타내는 사각형을 그려준다.
          }
          if Focused and (FDataLink.ActiveRecord = OldActive) then
            Canvas.DrawFocusRect(TmpRect);
          Canvas.Brush.Color := FCardColor;

          { 기타 필드를 ViewConfiguration에 정의된 순서대로 그려준다. }
          Canvas.Font.Assign(Self.Font);
          For nIndex := 0 To Pred(FViewConfiguration.Count) Do
          Begin
            If Length(FViewConfiguration[nIndex]) > 0 then
            Begin
              { 필드 이름 }
              szField := GetWord(FViewConfiguration[nIndex], 1, ',');

              { 필드가 그려질 영역 }
              FieldRect.Left := TmpRect.Left +
                StrToIntDef(GetWord(FViewConfiguration[nIndex], 2, ','), 0);
              FieldRect.Top := TmpRect.Bottom +
                StrToIntDef(GetWord(FViewConfiguration[nIndex], 3, ','), 0) + 3;
              FieldRect.Right := FieldRect.Left +
                StrToIntDef(GetWord(FViewConfiguration[nIndex], 4, ','), 0);
              FieldRect.Bottom := FieldRect.Top +
                StrToIntDef(GetWord(FViewConfiguration[nIndex], 5, ','), 0);

              FieldRect.Right := Min(CellRect.Right - 3, FieldRect.Right);
              FieldRect.Bottom := Min(CellRect.Bottom - 3, FieldRect.Bottom);

              { 필드를 그릴 때 사용할 문자열 형식 }
              szFormat := GetWord(FViewConfiguration[nIndex], 6, ',');
              DrawText(Canvas.Handle,
                       PChar(Format(szFormat,
                       [FDataLink.DataSet.FieldByName(szField).DisplayText])),
                       -1, FieldRect,
                       { 필드 정렬 방법 }
                       FieldAlignment[StrToIntDef(GetWord(FViewConfiguration[nIndex], 7, ','), 0)]
                       or DT_SINGLELINE or DT_VCENTER);
            End;
          End;
        end;
      end;
    end;
  end;
  if FDataLink.Active Then
    FDataLink.ActiveRecord := OldActive;
end;

function TdpbNameCard.VisibleRows: Integer;
begin
  Result := FColCount * FRowCount;
end;

procedure TdpbNameCard.DoVScroll(ScrollCode : TScrollCode);
var
  Offset : Integer;
begin
  Case ScrollCode of
    scLineUp :
      begin
        if not FDataLink.DataSet.BOF then
        begin
          if (FDataLink.ActiveRecord < FColCount) and
             (FDataLink.ActiveRecord > 0) then
          begin
            Offset := FDataLink.ActiveRecord;
            FDataLink.DataSet.MoveBy(-(FColCount + Offset));
            FDataLink.ActiveRecord := FDataLink.ActiveRecord + Offset;
          end
          else
            FDataLink.DataSet.MoveBy(-FColCount);
        end;
      end;
    scLineDown :
      begin
        if not FDataLink.DataSet.EOF then
        begin
          if (FDataLink.ActiveRecord div FColCount) = Pred(FRowCount) then
          begin
            Offset := FColCount - (FDataLink.ActiveRecord mod FColCount) - 1;
            FDataLink.DataSet.MoveBy(FColCount + Offset);
            FDataLink.ActiveRecord := FDataLink.ActiveRecord - Offset;
          end
          else
            FDataLink.DataSet.MoveBy(FColCount);
        end;
      end;
    scPageUp : FDataLink.DataSet.MoveBy(-VisibleRows);
    scPageDown : FDataLink.DataSet.MoveBy(VisibleRows);
    scTop : FDataLink.DataSet.First;
    scBottom : FDataLink.DataSet.Last;
  end;
end;

procedure TdpbNameCard.WMVScroll(var Message: TWMVScroll);
Begin
  With Message Do
  begin
    Case ScrollCode of
      SB_LINEUP, SB_LINEDOWN, SB_TOP, SB_BOTTOM, SB_PAGEUP, SB_PAGEDOWN :
        begin
          DoVScroll(TScrollCode(Message.ScrollCode));
        End;
      SB_THUMBPOSITION :
        begin
          FDataLink.DataSet.RecNo := Pos;
        End;
    End;
  End;
end;

procedure TdpbNameCard.SetVertScrollPos(const APos: Integer);
var
  sc : TScrollInfo;
Begin
  sc.cbSize := SizeOf(TScrollInfo);
  sc.fMask := SIF_POS Or SIF_DISABLENOSCROLL;
  sc.nPos := APos;
  SetScrollInfo(Handle, SB_VERT, sc, True);
end;

procedure TdpbNameCard.SetVertScrollRange(const AMin, AMax: Integer);
var
  sc : TScrollInfo;
Begin
  sc.cbSize := SizeOf(TScrollInfo);
  sc.fMask := SIF_RANGE or SIF_PAGE or SIF_DISABLENOSCROLL;
  sc.nMin := AMin;
  sc.nPage := VisibleRows;
  sc.nMax := AMax;
  SetScrollInfo(Handle, SB_VERT, sc, True);
end;

procedure TdpbNameCard.UpdateVertScrollBar;
begin
  if HandleAllocated then
  begin
    if FDatalink.Active then
    begin
      with FDataLink.DataSet do
      begin
        SetVertScrollRange(1, RecordCount + VisibleRows - 1);
        SetVertScrollPos(RecNo);
      end;
    end
    else
    begin
      with FDataLink.DataSet do
      begin
        SetVertScrollRange(1, 1);
        SetVertScrollPos(1);
      end;
    end;
  end;
end;

procedure TdpbNameCard.CreateWnd;
begin
  inherited CreateWnd;
  FDataLink.BufferCount := VisibleRows;
  UpdateVertScrollBar;
end;

procedure TdpbNameCard.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
var
  nCol, nRow : Integer;
begin
  inherited;
  SetFocus;
  if FDataLink.Active then
  begin
    { 마우스 포인터의 위치에 따라 현재 레코드를 조정한다. }
    for nRow := 1 To FRowCount do
    begin
      for nCol := 1 To FColCount do
      begin
        if PtInRect(GetCellRect(nCol, nRow), POINT(X, Y)) then
        begin
          FDataLink.DataSet.MoveBy(((nRow-1) * FColCount) + (nCol-1) -
                                   FDataLink.ActiveRecord);
          Exit;
        end;
      end;
    end;
  end;
end;

procedure TdpbNameCard.Changed(Sender: TObject);
begin
  Invalidate;
end;

{ 각 명함이 그려질 사각 영역을 구한다. }
function TdpbNameCard.GetCellRect(const ACol, ARow: Integer): TRect;
const
  MARGIN = 10;
var
  CellW, CellH : Integer;
begin
  CellW := (ClientWidth - ((FColCount + 1) * MARGIN)) div FColCount;
  CellH := (ClientHeight - ((FRowCount + 1) * MARGIN)) div FRowCount;

  Result.Left := (MARGIN * ACol) + (CellW * (ACol-1));
  Result.Top := (MARGIN * ARow) + (CellH * (ARow-1));
  Result.Right := Result.Left + CellW;
  Result.Bottom := Result.Top + CellH;
end;

procedure TdpbNameCard.WMEraseBkgnd(var Message: TMessage);
begin
  { 화면 깜박임을 줄여 준다. }
  Message.Result := 1;
end;

procedure TdpbNameCard.WMKillFocus(var Message: TWMKillFocus);
begin
  Invalidate;
end;

procedure TdpbNameCard.WMSetFocus(var Message: TWMSetFocus);
begin
  Invalidate;
end;

procedure TdpbNameCard.AdjustColRow;
begin
  FColCount := ClientWidth div FCardWidth;
  FRowCount := ClientHeight div FCardHeight;
  FDataLink.BufferCount := VisibleRows;
  UpdateVertScrollBar;
  Invalidate;
end;

procedure TdpbNameCard.Loaded;
begin
  inherited;
  AdjustColRow;
  ActiveChanged;
end;

procedure TdpbNameCard.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);
  if FDataLink.Active then
  begin
    case Key of
      VK_LEFT: FDataLink.DataSet.MoveBy(-1);
      VK_RIGHT: FDataLink.DataSet.MoveBy(1);
      VK_UP: DoVScroll(scLineUp);
      VK_DOWN: DoVScroll(scLineDown);
      VK_PRIOR: DoVScroll(scPageUp);
      VK_NEXT: DoVScroll(scPageDown);
      VK_HOME: DoVScroll(scTop);
      VK_END: DoVScroll(scBottom);
      VK_RETURN : DblClick;
    end;
  end;
end;

procedure TdpbNameCard.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  { 화살표 키를 입력 받을 수 있도록 한다. }
  Message.Result := DLGC_WANTARROWS;
end;

procedure TdpbNameCard.WMSize(var Message: TWMSize);
begin
  inherited;
  AdjustColRow;
end;

end.


먼저 TdpbNameCard 콤포넌트는 인맥 테이블의 여러 레코드를 열람하므로 TDataLink에서 상속 받은 TdpbNameCardDataLink 클래스를 만든다.
TdpbNameCardDataLink는 TDataLink 클래스의 ActiveChanged와 DataChanged 메소드를 오버라이드해서 DataSet의 Active 프로퍼티가 변경되거나 DataSet의 현재 레코드가 변경되는 상황을 콤포넌트가 자동으로 반영할 수 있도록 만들었다.
다음 TdpbNameCard의 생성자에서 TdpbNameCardDataLink의 인스턴스를 생성하고 소멸자에서 인스턴스를 해제 하도록 해서 TdpbNameCard 콤포넌트가 생성되면 데이터 링크도 만들어 지고 콤포넌트가 소멸되면 데이터 링크도 소멸되도록 만들었다. 모든 데이터 연동 콤포넌트는 DataSource라는 프로퍼티를 제공해서 내부 데이터 링크와 TDataSource 콤포넌트를 연결할 수 있도록 하고 있다. 따라서 TdpbNameCard도 DataSource 프로퍼티를 만들어 준다. DataSource 프로퍼티는 별도 멤버 변수를 만드는 것이 아니고 FDataLink의 DataSource 프로퍼티를 콤포넌트 사용자에게 노출시켜 주는 프로퍼티이므로 GetDataSoruce 읽기 메소드와 SetDataSource 쓰기 메소드를 제공해서 FDataLink의 DataSource 프로퍼티를 조작하도록 한다. DataSource 프로퍼티는 TDataSource 콤포넌트에 대한 레퍼런스이므로 폼 설계 시 TDataSource 콤포넌트가 삭제될 경우를 대비해서 Notification 메소드를 오버라이드해서 적절한 처리를 해주도록 한다.
여기까지 하면 콤포넌트의 데이터 링크와 데이터 소스가 통신할 수 있는 채널은 모두 만들어 주는 것이 된다. 나머지 인맥 데이터를 실제로 화면에 그려 주는 일 등은 일반 콤포넌트를 만들 때와 같이 개발하면 된다. TdpbNameCard는 데이터 소스의 레코드 개수에 따라 수직 스크롤바가 필요하므로 CreateParams 메소드를 오버라이드해서 윈도 스타일에 WS_VSCROLL을 추가해 주고 콤포넌트의 외곽선을 보기 좋게 하기 위해 WS_EXCLIENTEDGE나 WS_BORDER 스타일을 추가해 준다.
그런데 소스 코드를 보면 이상한 메시지(CM_CTL3DCHANGED) 하나를 처리하는 것을 볼 수 있을 것이다. Messages.pas를 찾아 봐도 그런 메시지는 없다. 이런 메시지를 콤포넌트 메시지라고 하는데 윈도 시스템 메시지가 아니고 델파이에서 별도로 정의한 커스텀 메시지이다. 콤포넌트 메시지에는 콤포넌트 개발자에게 유용한 메시지들이 많은데 60여개의 메시지 중에 중요한 메시지를 아래 표에 나타내었다. 콤포넌트 메시지는 controls.pas에 정의되어 있다.


표 2-5 콤포넌트 메시지
메시지설명
CM_PARENTFONTCHANGEDParentFont 프로퍼티가 변경되었다.
CM_PARENTCOLORCHANGEDParentColor 프로퍼티가 변경되었다.
CM_VISIBLECHANGEDVisible 프로퍼티가 변경되었다.
CM_ENABLEDCHANGEdEnabled 프로퍼티가 변경되었다.
CM_COLORCHANGEDColor 프로퍼티가 변경되었다.
CM_FONTCHANGEDFont 프로퍼티가 변경되었다.
CM_CTL3DCHANGEDCtl3D 프로퍼티가 변경되었다.
CM_TEXTCHANGEDText나 Caption 프로퍼티가 변경되었다.
CM_MOUSEENTER마우스 포인터가 콤포넌트 영역 내에 있다.
CM_MOUSELEAVE마우스 포인터가 콤포넌트 영역을 벗어 났다.
CM_DESIGNHITTEST설계 시에 마우스 포인터가 콤포넌트 영역 내에서 움직이면 발생한다. 이 메시지의 Result 값을 1로 주면 WM_MOUSEMOVE 메시지가 발생한다. 이 메시지를 이용하면 설계 시에도 실행 시처럼 동작하게 만들 수 있다. TDBGrid가 이 메시지를 아주 유용하게 사용한다.
CM_WANTSPECIALKEY특수 키(화살표, 탭, Enter 등)이 눌려지면 발생한다.
CM_GETDATALINKTDBCtrlGrid가 자신이 포함하고 있는 데이터 연동 콤포넌트들에게 각 콤포넌트의 데이터 링크를 구하기 위해 보내는 메시지이다.


2.5.4. 데이터 수정(Data Editing)
데이터를 수정할 수 있는 데이터 연동 콤포넌트를 만드는 것은 데이터 열람 기능에 더해서 편집하고 업데이트 하는 기능을 추가해 주는 것이다. 데이터 수정 기능을 추가하는 과정을 설명하기 위해 TdpbDBNumberEdit라는 콤포넌트를 만들것이다. TdpbDBNumberEdit는 TCustomEdit에서 상속받아서 만드는데 키보드로 텍스트를 입력할 때 숫자만 받아들이도록 할 것이다. 완성된 콤포넌트와 사용 예제를 아래 리스트와 그림으로 나타내었다.

그림 2-14 dpbDBNumberEdit 예제 실행 화면


TdpbDBNumberEdit는 다른 데이터 열람 콤포넌트와 마찬가지로 데이터의 현재 레코드가 이동되면 즉시 현재 레코드의 값을 콤포넌트의 Value 값으로 반영한다. 그리고 콤포넌트의 Value 값이 변경된 상태에서 레코드가 이동하려 하거나 입력 포커스를 잃을 때 자신의 값을 현재 레코드의 필드 값으로 저장한다.

리스트 2.6 DpbDBNumberEdit.pas
unit dpbDBNumberEdit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, db, dbctrls;

type
  TdpbDBNumberEdit = class(TCustomEdit)
  private
    FMinValue: LongInt;
    FMaxValue: LongInt;
    FDataLink : TFieldDataLink;

    function GetValue: LongInt;
    function CheckValue (NewValue: LongInt): LongInt;
    procedure SetValue (NewValue: LongInt);
    procedure CMEnter(var Message: TCMGotFocus); message CM_ENTER;
    procedure CMExit(var Message: TCMExit);   message CM_EXIT;
    procedure WMUndo(var Message: TMessage); message WM_UNDO;
    procedure WMPaste(var Message: TWMPaste);   message WM_PASTE;
    procedure WMCut(var Message: TWMCut);   message WM_CUT;
    procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK;
    function GetDataField: string;
    function GetDataSource: TDataSource;
    procedure SetDataField(const Value: string);
    procedure SetDataSource(const Value: TDataSource);

    procedure DataChange(Sender: TObject);
    procedure EditingChange(Sender: TObject);
    procedure UpdateData(Sender: TObject);
    function GetReadOnly: Boolean;
    procedure SetReadOnly(const Value: Boolean);

  protected
    procedure Change; override;
    procedure Notification(AComponent: TComponent;
          Operation: TOperation); override;
    function IsValidChar(Key: Char): Boolean; virtual;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure KeyPress(var Key: Char); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Anchors;
    property AutoSelect;
    property AutoSize;
    property Color;
    property Constraints;
    property Ctl3D;
    property DataSource : TDataSource read GetDataSource write SetDataSource;
    property DataField : string read GetDataField write SetDataField;
    property DragCursor;
    property DragMode;
    property Enabled;
    property Font;
    property MaxLength;
    property MaxValue: LongInt read FMaxValue write FMaxValue default 0;
    property MinValue: LongInt read FMinValue write FMinValue default 0;
    property ParentColor;
    property ParentCtl3D;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ReadOnly: Boolean read GetReadOnly write SetReadOnly default False;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property Value: LongInt read GetValue write SetValue;
    property Visible;

    property OnChange;
    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDrag;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('dpb', [TdpbDBNumberEdit]);
end;

{ TdpbDBNumberEdit }

constructor TdpbDBNumberEdit.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  { TDBCtrlGrid에서 사용하려면 csReplicatable 플래그가 설정되어 있어야 한다. }
  ControlStyle := ControlStyle - [csSetCaption] + [csReplicatable];

  FMinValue := 0;
  FMaxValue := 0;

  { 데이터 링크를 생성한다. }
  FDataLink := TFieldDataLink.Create;
  { TField 객체의 FocusControl 프로퍼티와 데이터 연동 콤포넌트를 연결하기 위해
    데이터 링크의 Control 프로퍼티를 자신으로 설정한다. } 
  FDataLink.Control := Self;
  FDataLink.OnDataChange := DataChange;
  FDataLink.OnEditingChange := EditingChange;
  FDataLink.OnUpdateData := UpdateData;

end;

destructor TdpbDBNumberEdit.Destroy;
begin
  FDataLink.Free;
  inherited Destroy;
end;

procedure TdpbDBNumberEdit.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);

  { 글자 삭제나 삽입시 데이터 링크를 수정 모드로 }
  if (Key = VK_DELETE) or ((Key = VK_INSERT) and (ssShift in Shift)) then
    FDataLink.Edit;
end;

procedure TdpbDBNumberEdit.KeyPress(var Key: Char);
begin
  { 유효한 키인지 조사한다. }
  if not IsValidChar(Key) then
  begin
    Key := #0;
    MessageBeep(0);
  end;

  case Key of
    { 유효한 키인 경우 데이터 링크를 수정 모드로 변경한다. }
    ^H, ^V, ^X, '0'..'9', '+', '-':
      FDataLink.Edit;
    #27:
      begin
        { ESC 키인 경우 Reset }
        FDataLink.Reset;
        SelectAll;
        Key := #0;
      end;
  end;
  if Key <> #0 then inherited KeyPress(Key);
end;

function TdpbDBNumberEdit.IsValidChar(Key: Char): Boolean;
begin
  Result := (Key in ['+', '-', '0'..'9']) or
            ((Key < #32) and (Key <> Chr(VK_RETURN)));
end;

procedure TdpbDBNumberEdit.WMUndo(var Message: TMessage);
begin
  FDataLink.Edit;
  inherited;
end;

procedure TdpbDBNumberEdit.WMPaste(var Message: TWMPaste);
begin
  FDataLink.Edit;
  inherited;
end;

procedure TdpbDBNumberEdit.WMCut(var Message: TWMPaste);
begin
  FDataLink.Edit;
  inherited;
end;

procedure TdpbDBNumberEdit.CMExit(var Message: TCMExit);
begin
  inherited;

  if CheckValue (Value) <> Value then
    SetValue (Value);

  { 입력 포커스를 잃을 때 변경된 값을 업데이트 한다. }
  try
    FDataLink.UpdateRecord;
  except
    { 업데이트에 실패하면 포커스를 다시 가져 온다. }
    SelectAll;
    SetFocus;
    raise;
  end;
end;

procedure TdpbDBNumberEdit.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (FDataLink <> nil) and
    (AComponent = DataSource) then DataSource := nil;
end;

function TdpbDBNumberEdit.GetValue: LongInt;
begin
  Result := StrToIntDef(Text, FMinValue);
end;

procedure TdpbDBNumberEdit.SetValue (NewValue: LongInt);
begin
  Text := IntToStr (CheckValue (NewValue));
end;

function TdpbDBNumberEdit.CheckValue (NewValue: LongInt): LongInt;
begin
  Result := NewValue;
  if (FMaxValue <> FMinValue) then
  begin
    if NewValue < FMinValue then
      Result := FMinValue
    else if NewValue > FMaxValue then
      Result := FMaxValue;
  end;
end;

procedure TdpbDBNumberEdit.CMEnter(var Message: TCMGotFocus);
begin
  if AutoSelect and not (csLButtonDown in ControlState) then
    SelectAll;
  inherited;
end;

function TdpbDBNumberEdit.GetDataField: string;
begin
  Result := FDataLink.FieldName;
end;

function TdpbDBNumberEdit.GetDataSource: TDataSource;
begin
  Result := FDataLink.DataSource;
end;

procedure TdpbDBNumberEdit.SetDataField(const Value: string);
begin
    FDataLink.FieldName := Value;
end;

procedure TdpbDBNumberEdit.SetDataSource(const Value: TDataSource);
begin
  if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then
    FDataLink.DataSource := Value;
  if Value <> nil then Value.FreeNotification(Self);
end;

procedure TdpbDBNumberEdit.CMGetDataLink(var Message: TMessage);
begin
  Message.Result := Integer(FDataLink);
end;

function TdpbDBNumberEdit.GetReadOnly: Boolean;
begin
  Result := FDataLink.ReadOnly;
end;

procedure TdpbDBNumberEdit.SetReadOnly(const Value: Boolean);
begin
  FDataLink.ReadOnly := Value;
end;

procedure TdpbDBNumberEdit.DataChange(Sender: TObject);
begin
  { 현재 필드의 값을 콤포넌트에 반영한다. }
  if FDatalink.Field <> nil then
    Value := FDataLink.Field.AsInteger
  else
  begin
    if csDesigning in ComponentState then
      Text := Name
    else
      Text := '';
  end;
end;

procedure TdpbDBNumberEdit.EditingChange(Sender: TObject);
begin
  inherited ReadOnly := not FDataLink.Editing;
end;

procedure TdpbDBNumberEdit.UpdateData(Sender: TObject);
begin
  { 실제로 필드의 값을 업데이트 한다. }
  FDataLink.Field.AsInteger := Value;
end;

procedure TdpbDBNumberEdit.Change;
begin
  if FDataLink <> nil then
    FDataLink.Modified;
  inherited Change;
end;

end.


데이터 연동을 위해 여러 개의 필드를 참조하는 TdpbNameCard와 달리 TdpbDBNumberEdit는 하나의 필드만 연결하므로 TFieldDataLink 객체를 사용한다. TFieldDataLink는 TDataLink와 달리 데이터에서 발생하는 사건을 이벤트로 정의해 놓았기 때문에 TdpbNameCard에서 처럼 가상 메소드를 오버라이드 하지 않고 이벤트 핸들러를 작성해 주면 된다. 그리고 DataSource 프로퍼티와 연동할 데이터베이스 필드를 지정하기 위해 DataField 프로퍼티를 새로 추가해 준다. 다음 데이터 수정을 위한 콤포넌트들이 대부분 가지고 있는 ReadOnly 프로퍼티를 FDataLink의 ReadOnly 프로퍼티를 이용해서 구현한다. ReadOnly 프로퍼티는 해당 필드의 값을 변경하지 못하도록 하는데 사용한다. 그런데 TdpbDBNumberEdit의 부모 클래스인 TCustomEdit는 이미 ReadOnly 프로퍼티를 가지고 있다. 이 ReadOnly 프로퍼티는 콤포넌트가 가지고 있는 Text 값을 사용자가 변경하지 못하도록 하는데 사용한다. 따라서 데이터 링크의 필드 값을 ReadOnly로 설정하는 것과 콤포넌트 자신의 값을 ReadOnly로 하는 것을 동기화 해야할 필요가 있다. 그래서 EditingChange 이벤트 핸들러에서 데이터 링크의 Editing 프로퍼티가 변경될 때 마다 콤포넌트 원래의 ReadOnly 프로퍼티도 설정되도록 했다.
DataChange 이벤트 핸들러에서는 현재 필드의 값을 콤포넌트의 Value 프로퍼티에 반영하는데 FDataLink의 Field 프로퍼티가 아직 설정되지 않았고 설계 모드인 경우에는 콤포넌트의 이름을 보여 주도록 했다.
다음에 사용자가 콤포넌트의 값을 변경하기 위해 키보드를 누를 때 마다 호출되는 KeyPress 메소드에서 입력한 키가 유효한 키인지 조사하고 유효하지 않은 키인 경우에는 그 키를 무시하고 경고음을 내도록 만들었다. 그리고 유효한 키가 입력되었을 경우에는 데이터 링크를 편집 모드로 변경해 준다.
콤포넌트가 키보드 입력 포커스를 잃을 때 발생하는 CM_EXIT 메시지 핸들러에서는 변경된 Value 값을 데이터 링크의 필드 값에 저장한다. 저장에 실패할 경우에는 잃었던 포커스를 다시 찾아 와서 필드 값을 다시 입력할 수 있도록 해 준다.
마지막으로 콤포넌트의 Value 값이 변경될 때 마다 OnChange 이벤트를 발생시키기 위해서 Change 가상 메소드가 호출되는데 이 메소드를 오버라이드해서 데이터 링크가 다른 레코드로 이동할 때 Value 값을 업데이트 할 수 있도록 FDataLink의 Modified 프로퍼티를 설정해준다.

데이터 연동 콤포넌트 개발은 언뜻 복잡해 보이지만 데이터 링크와 콤포넌트가 연결되는 개념을 잘 파악해서 데이터 연동 콤포넌트의 동작 원리가 익숙해지면 아마 복잡하다 느끼기 보다는 지루한 작업이라는 것을 알게 될 것이다. 데이터 연동 콤포넌트 개발의 핵심은 데이터 링크라는 것을 명심해야 할 것이다.


저작권에 대한 공지
이 문서에 포함된 모든 글과 소스 코드는 작자인 저 김성동의 지적 재산이지만 무단으로 배포하거나 책이나 강의 교재등을 위한 복제 등 저작권을 저해하는 행위를 제외하고는 자유롭게 사용할 수 있습니다.
또한 이 페이지에 대한 링크는 자유롭게 할 수 있으나 전문을 전제하는 일은 허용하지 않습니다.