Доступен ли элемент управления вкладками с учетом данных?

Элементы управления с поддержкой данных могут быть связаны с наборами данных для отображения данных, содержащихся в полях в текущей строке, или, в некоторых случаях, данных из одного или нескольких столбцов в нескольких строках. А TTabControl позволяет применять один и тот же набор элементов управления к разным наборам значений данных простым для понимания способом.

Мне кажется, они бы хорошо ладили друг с другом. TTabControl мог бы стать хорошим элементом управления с учетом данных (свяжите его со столбцом идентификаторов в наборе данных, и он мог бы быть гораздо более интуитивно понятным навигатором, чем TDBNavigator), но его нет в VCL.

Кто-нибудь создал элемент управления вкладками с учетом данных? Единственное, что я нашел, это DBTABCONTROL98 Жана-Люка Маттеи, который восходит к 1998 году (эра Delphi 3) и даже после его модификации для компиляции под XE на самом деле не работает. Есть ли другие, которые работают должным образом? (т.е. добавление / удаление вкладок при добавлении / удалении новых записей из набора данных и переключение активной строки набора данных при изменении вкладок пользователем и наоборот.)

И да, я знаю, что это может стать немного громоздким, если в наборе данных много строк. Я ищу что-нибудь для создания пользовательского интерфейса для случая использования, когда количество строк выражается однозначными или очень маленькими двузначными числами.


person Mason Wheeler    schedule 20.03.2012    source источник
comment
Я бы назвал это набором вкладок вместо элемента управления вкладками, поскольку вы хотели бы использовать одну клиентскую область (элементы управления внутри).   -  person Warren P    schedule 20.03.2012
comment
данные из нескольких строк. Но меня здесь волнует не это. Думаю, вы говорите о данных из нескольких строк! Вам нужна DBGrid, где индекс строки прикреплен к вкладкам, а столбцы прикреплены к элементам управления под этими вкладками.   -  person NGLN    schedule 20.03.2012
comment
@NGLN: Хороший момент. То, что я думал, когда писал, не совсем соответствовало тому, что я написал. Отредактировано.   -  person Mason Wheeler    schedule 21.03.2012
comment
@Warren: Я называю это вкладкой, потому что VCL называет это именно так.   -  person Mason Wheeler    schedule 21.03.2012
comment
НЕТ, элемент управления вкладкой содержит X страниц, и каждая страница содержит разные элементы управления. Это то, что Vcl называет вкладкой. Он также содержит набор вкладок, и именно так его называет VCL, когда у вас есть вкладки, не содержащие страниц.   -  person Warren P    schedule 21.03.2012
comment
@WarrenP: Думаю, ты думаешь о TPageControl. TTabControl именно то, что я описал. А TTabSet - это нечто совершенно иное; это в основном просто вкладки без контейнера страницы.   -  person Mason Wheeler    schedule 21.03.2012


Ответы (1)


Я написал для вас TDBTabControl. Если вы не установите свойство DataField, то заголовки вкладок будут индексом записи. Вкладка, отмеченная звездочкой, указывает на новую запись, видимость которой можно переключать с помощью свойства ShowInsertTab.

Я унаследовал от TCustomTabControl, потому что свойства Tabs, TabIndex и MultiSelect не могут быть опубликованы для этого компонента.

TDBTabControl Demo

unit DBTabControl;

interface

uses
  Classes, Windows, SysUtils, Messages, Controls, ComCtrls, DB, DBCtrls;

type
  TCustomDBTabControl = class(TCustomTabControl)
  private
    FDataLink: TFieldDataLink;
    FPrevTabIndex: Integer;
    FShowInsertTab: Boolean;
    procedure ActiveChanged(Sender: TObject);
    procedure DataChanged(Sender: TObject);
    function GetDataField: String;
    function GetDataSource: TDataSource;
    function GetField: TField;
    procedure RebuildTabs;
    procedure SetDataField(const Value: String);
    procedure SetDataSource(Value: TDataSource);
    procedure SetShowInsertTab(Value: Boolean);
    procedure CMExit(var Message: TCMExit); message CM_EXIT;
    procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK;
  protected
    function CanChange: Boolean; override;
    procedure Change; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
    procedure Loaded; override;
    property DataField: String read GetDataField write SetDataField;
    property DataSource: TDataSource read GetDataSource write SetDataSource;
    property Field: TField read GetField;
    property ShowInsertTab: Boolean read FShowInsertTab write SetShowInsertTab
      default False;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function ExecuteAction(Action: TBasicAction): Boolean; override;
    function UpdateAction(Action: TBasicAction): Boolean; override;
  end;

  TDBTabControl = class(TCustomDBTabControl)
  public
    property DisplayRect;
    property Field;
  published
    property Align;
    property Anchors;
    property BiDiMode;
    property Constraints;
    property DockSite;
    property DataField;
    property DataSource;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property HotTrack;
    property Images;
    property MultiLine;
    property OwnerDraw;
    property ParentBiDiMode;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property RaggedRight;
    property ScrollOpposite;
    property ShowHint;
    property ShowInsertTab;
    property Style;
    property TabHeight;
    property TabOrder;
    property TabPosition;
    property TabStop;
    property TabWidth;
    property Visible;
    property OnChange;
    property OnChanging;
    property OnContextPopup;
    property OnDockDrop;
    property OnDockOver;
    property OnDragDrop;
    property OnDragOver;
    property OnDrawTab;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnGetImageIndex;
    property OnGetSiteInfo;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnResize;
    property OnStartDock;
    property OnStartDrag;
    property OnUnDock;
  end;

implementation

{ TCustomDBTabControl }

procedure TCustomDBTabControl.ActiveChanged(Sender: TObject);
begin
  RebuildTabs;
end;

function TCustomDBTabControl.CanChange: Boolean;
begin
  FPrevTabIndex := TabIndex;
  Result := (inherited CanChange) and (DataSource <> nil) and
    (DataSource.State in [dsBrowse, dsEdit, dsInsert]);
end;

procedure TCustomDBTabControl.Change;
var
  NewTabIndex: Integer;
begin
  try
    if FDataLink.Active and (DataSource <> nil) then
    begin
      if FShowInsertTab and (TabIndex = Tabs.Count - 1) then
        DataSource.DataSet.Append
      else if DataSource.State = dsInsert then
      begin
        NewTabIndex := TabIndex;
        DataSource.DataSet.CheckBrowseMode;
        DataSource.DataSet.MoveBy(NewTabIndex - TabIndex);
      end
      else
        DataSource.DataSet.MoveBy(TabIndex - FPrevTabIndex);
    end;
    inherited Change;
  except
    TabIndex := FPrevTabIndex;
    raise;
  end;
end;

procedure TCustomDBTabControl.CMExit(var Message: TCMExit);
begin
  try
    FDataLink.UpdateRecord;
  except
    SetFocus;
    raise;
  end;
  inherited;
end;

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

constructor TCustomDBTabControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FDataLink := TFieldDataLink.Create;
  FDataLink.Control := Self;
  FDataLink.OnActiveChange := ActiveChanged;
  FDataLink.OnDataChange := DataChanged;
end;

procedure TCustomDBTabControl.DataChanged(Sender: TObject);
const
  StarCount: array[Boolean] of Integer = (0, 1);
var
  NewTabIndex: Integer;
begin
  if FDataLink.Active and (DataSource <> nil) then
    with DataSource do
    begin
      if DataSet.RecordCount <> Tabs.Count - StarCount[FShowInsertTab] then
        RebuildTabs
      else if (State = dsInsert) and FShowInsertTab then
        TabIndex := Tabs.Count - 1
      else if Tabs.Count > 0 then
      begin
        NewTabIndex := Tabs.IndexOfObject(TObject(DataSet.RecNo));
        if (TabIndex = NewTabIndex) and (State <> dsInsert) and
            (Field <> nil) and (Field.AsString <> Tabs[TabIndex]) then
          Tabs[TabIndex] := Field.AsString;
        TabIndex := NewTabIndex;
      end;
    end;
end;

destructor TCustomDBTabControl.Destroy;
begin
  FDataLink.Free;
  FDataLink := nil;
  inherited Destroy;
end;

function TCustomDBTabControl.ExecuteAction(Action: TBasicAction): Boolean;
begin
  Result := inherited ExecuteAction(Action) or FDataLink.ExecuteAction(Action);
end;

function TCustomDBTabControl.GetDataField: String;
begin
  Result := FDataLink.FieldName;
end;

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

function TCustomDBTabControl.GetField: TField;
begin
  Result := FDataLink.Field;
end;

procedure TCustomDBTabControl.KeyDown(var Key: Word; Shift: TShiftState);
begin
  if (DataSource <> nil) and (DataSource.State = dsInsert) and
    (Key = VK_ESCAPE) then
  begin
    DataSource.DataSet.Cancel;
    Change;
  end;
  inherited keyDown(Key, Shift);
end;

procedure TCustomDBTabControl.Loaded;
begin
  inherited Loaded;
  if (csDesigning in ComponentState) then
    RebuildTabs;
end;

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

procedure TCustomDBTabControl.RebuildTabs;
var
  Bookmark: TBookmark;
begin
  if (DataSource <> nil) and (DataSource.State = dsBrowse) then
    with DataSource do
    begin
      if HandleAllocated then
        LockWindowUpdate(Handle);
      Tabs.BeginUpdate;
      DataSet.DisableControls;
      BookMark := DataSet.GetBookmark;
      try
        Tabs.Clear;
        DataSet.First;
        while not DataSet.Eof do
        begin
          if Field = nil then
            Tabs.AddObject(IntToStr(Tabs.Count + 1), TObject(DataSet.RecNo))
          else
            Tabs.AddObject(Field.AsString, TObject(DataSet.RecNo));
          DataSet.Next;
        end;
        if FShowInsertTab then
          Tabs.AddObject('*', TObject(-1));
      finally
        DataSet.GotoBookmark(Bookmark);
        DataSet.FreeBookmark(Bookmark);
        DataSet.EnableControls;
        Tabs.EndUpdate;
        if HandleAllocated then
          LockWindowUpdate(0);
      end;
    end
  else
    Tabs.Clear;
end;

procedure TCustomDBTabControl.SetDataField(const Value: String);
begin
  FDataLink.FieldName := Value;
  RebuildTabs;
end;

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

procedure TCustomDBTabControl.SetShowInsertTab(Value: Boolean);
begin
  if FShowInsertTab <> Value then
  begin
    FShowInsertTab := Value;
    RebuildTabs;
  end;
end;

function TCustomDBTabControl.UpdateAction(Action: TBasicAction): Boolean;
begin
  Result := inherited UpdateAction(Action) or FDataLink.UpdateAction(Action);
end;

end.

unit DBTabControlReg;

interface

uses
  Classes, DBTabControl;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TDBTabControl]);
end;

end.

package DBTabControl70;

{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO ON}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION OFF}
{$OVERFLOWCHECKS ON}
{$RANGECHECKS ON}
{$REFERENCEINFO ON}
{$SAFEDIVIDE OFF}
{$STACKFRAMES ON}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST OFF}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DESCRIPTION '#DBTabControl'}
{$IMPLICITBUILD OFF}

requires
  rtl,
  vcl,
  dbrtl,
  vcldb;

contains
  DBTabControl in 'DBTabControl.pas',
  DBTabControlReg in 'DBTabControlReg.pas';

end.
person NGLN    schedule 20.03.2012
comment
Выглядит отлично, за исключением пары вещей: нет свойств Align или TabOrder (существовали ли они еще в D7? Они есть в XE), и возможность правильно отслеживать вставки и удаления строк очень важна для моего варианта использования. Но это похоже на хорошую отправную точку. Я немного поработаю и посмотрю, смогу ли я построить что-нибудь еще дальше ... - person Mason Wheeler; 21.03.2012
comment
+1 и добавил в закладки. Спасибо за то, что написали эти хорошие компоненты для публики. - person Justmade; 21.03.2012
comment
@Mason Вы пропустили TODO в коде ...;) Я добавлю обработку изменения количества записей. Завтра. ;) - person NGLN; 21.03.2012
comment
@Mason Теперь он также отслеживает вставки и удаления. Развлекайся. (Ох уж эта отвратительная зависимость, не мог заснуть!) - person NGLN; 21.03.2012
comment
@NGLN: Так лучше, но пользовательский интерфейс, созданный с его помощью, непригоден для использования: если я редактирую какие-либо элементы управления, связанные с тем же набором данных, или использую вкладку *, я внезапно не могу переключать вкладки! В методах CanChange и Change он должен иметь возможность принимать DataSource.State in [dsBrowse, dsEdit, dsInsert], а не только = dsBrowse. Не уверен, нужно ли изменить и другие места в коде, выполняющие ту же проверку, или нет. Однако, если я поменяю эти два, это сработает. - person Mason Wheeler; 21.03.2012
comment
@Mason Да, это было временное решение, потому что я не мог понять, как отменить изменение вкладки в случае ошибки проверки данных. Теперь я использую блок try-except в методе Change, но я не совсем уверен, гарантирует ли это сброс индекса вкладки в случае обработки исключений в другом месте приложения. Кроме того, компонент к настоящему времени отлажен и отремонтирован ...;) - person NGLN; 21.03.2012
comment
@NGLN: Отличная работа. Всего пара мелких недочетов. Именование вкладок не всегда согласовано (создайте кнопку, которая при нажатии добавляет новую строку в набор данных, а в связанном поле элемента управления вкладками, пусть она ведет счет: 1, 2, 3 и т. Д. Нажмите ее несколько раз. раз, и вы увидите повторяющиеся числа в метках вкладок, что не соответствует тому, что на самом деле находится в наборе данных.) Кроме того, поведение вкладки * выглядит странным. Щелкните его, измените некоторые данные в связанных элементах управления, переключитесь на другую вкладку и вернитесь на вкладку *, и ваши изменения исчезнут. - person Mason Wheeler; 21.03.2012
comment
И было бы очень хорошо иметь возможность либо отключить вкладку *, либо поместить на нее обработчик событий, поскольку вполне возможно, что приложение будет иметь внутренние правила, которым должны соответствовать новые строки. Просто слепой вызов Dataset.Append может нарушить это. - person Mason Wheeler; 21.03.2012
comment
@Mason Я добавил свойство ShowInsertTab. Хотя никаких событий, я бы предпочел DataSource.OnStateChange для этого. Изменения на вкладке * теперь сохраняются. Я не могу воспроизвести описанную вами проблему именования вкладок. Что вы имели в виду под вести счет? - person NGLN; 21.03.2012
comment
+1. Я тоже вчера начал писать этот компонент (скелет был фактически TDBListBox), но после того, как вы разместили код, я сбросил его. всего несколько комментариев: не лучше ли хранить TObject(bookmark) вместо RecNo? что произойдет, если набор данных будет отфильтрован или отсортирован? Вы это учли? - person kobik; 21.03.2012
comment
@NGLN: Как и в случае, первая вкладка получает имя 1, вторая - 2 и т. Д. Я сделал это с агрегатом CDS, потому что это одна часть больших требований к дизайну, но для целей тестирования вы, вероятно, могли бы просто вставить значение dataset.RecordCount + 1. - person Mason Wheeler; 21.03.2012
comment
Кроме того, новая версия выдает ошибку, как только я открываю форму: Exception class EListError with message 'Failed to retrieve tab at index -1', исходящую из вызова IndexOfObject в строке 178. Похоже, есть еще несколько вещей, которые нужно решить. Не могли бы вы встретиться со мной в чате SO, чтобы облегчить обсуждение? Это было бы проще, чем использовать все эти комментарии взад и вперед ... - person Mason Wheeler; 21.03.2012
comment
comment
@kobik Отфильтрованные наборы данных идут хорошо: как только набор данных откроется, вкладки будут перестроены. Закладки после освобождения берут на себя немного больше административных задач. Кроме того, мне не удалось получить достоверное сравнение закладок, но это может быть моей ошибкой или неопытностью. Думаю, надо проверить сортировку. - person NGLN; 21.03.2012