unit ListViewColProperties; interface uses Classes, ComCtrls, Contnrs; type TCustomListViewColProperty = class(TObject) Alignment: TAlignment; Caption: string; Width: Integer; MaxWidth: Integer; MinWidth: Integer; Visible: Boolean; Order: Integer; constructor Create(AOrder: Integer); end; type TCustomListViewColProperties = class(TPersistent) private FChanged: Boolean; FOnChange: TNotifyEvent; FUpdating: Integer; FProperties: TObjectList; FCreated: Boolean; function GetColumns: TListColumns; function GetCount: Integer; function GetOrderStr: string; procedure CheckBounds(Index: Integer); procedure SetWidthsStr(Value: string; PixelsPerInch: Integer); function GetWidthsStr: string; procedure SetOrderStr(Value: string); protected FListView: TCustomListView; FListViewManaged: Boolean; FConstraintsInitialized: Boolean; function GetAlignments(Index: Integer): TAlignment; function GetParamsStr: string; virtual; function GetVisible(Index: Integer): Boolean; function GetWidths(Index: Integer): Integer; procedure SetAlignments(Index: Integer; Value: TAlignment); procedure SetVisibleInternal(Index: Integer; Value: Boolean; SaveWidth: Boolean); procedure SetVisible(Index: Integer; Value: Boolean); procedure SetWidths(Index: Integer; Value: Integer); function GetCaptions(Index: Integer): string; procedure Changed; virtual; procedure SetCaptions(Index: Integer; Value: string); virtual; procedure SetParamsStr(Value: string); virtual; procedure UpdateListView; procedure UpdateFromListView; procedure UpdateOrderFromListView; procedure UpdateListViewOrder; function GetProperties(Index: Integer): TCustomListViewColProperty; function GetIndexByOrder(Order: Integer): Integer; function ColumnsExists: Boolean; procedure SetRuntimeVisible(Index: Integer; Value: Boolean; SaveWidth: Boolean); function GetColumn(Index: Integer): TListColumn; procedure CreateProperties(ACount: Integer); function DefaultConstraint(Value: Integer; Visible: Boolean; Def: Integer): Integer; property Columns: TListColumns read GetColumns stored False; public constructor Create(ListView: TCustomListView; ColCount: Integer); destructor Destroy; override; procedure EndUpdate; procedure BeginUpdate; procedure ListViewWndCreated; procedure ListViewWndDestroying; procedure ListViewWndDestroyed; procedure ChangeScale(M, D: Integer); property Count: Integer read GetCount stored False; property Alignments[Index: Integer]: TAlignment read GetAlignments write SetAlignments; property Captions[Index: Integer]: string read GetCaptions write SetCaptions; property Widths[Index: Integer]: Integer read GetWidths write SetWidths; property Visible[Index: Integer]: Boolean read GetVisible write SetVisible; procedure RecreateColumns; property OnChange: TNotifyEvent read FOnChange write FOnChange; property ParamsStr: string read GetParamsStr write SetParamsStr stored False; end; { TCustomListViewColProperties } type TListViewColProperties = class(TCustomListViewColProperties) published end; { TListViewColProperties } implementation uses SysUtils, CommCtrl, Windows, PasTools, Controls, Forms; const DefaultListViewMaxWidth = 1000; DefaultListViewMinWidth = 20; { TODO : V ListView zamezit zmenu velikosti neviditelnych sloupecku } constructor TCustomListViewColProperty.Create(AOrder: Integer); begin Alignment := taLeftJustify; Caption := ''; Width := 50; Visible := True; Order := AOrder; end; { TCustomListViewColProperties } constructor TCustomListViewColProperties.Create( ListView: TCustomListView; ColCount: Integer); var ACount: Integer; begin // This contructor (and constructors of descendants) // is only even called from implementations of // TCustomNortonLikeListView.NewColProperties inherited Create; FConstraintsInitialized := False; FCreated := False; FUpdating := 0; FChanged := False; // ColCount is not 0 for file panels (TDirView and TCustomUnixDirView). // It is 0 otherwise. FListViewManaged := (ColCount = 0); FListView := ListView; FProperties := TObjectList.Create; if FListViewManaged then ACount := GetColumns.Count else ACount := ColCount; CreateProperties(ACount); if not Assigned(FListView) then raise Exception.Create('NIL ListView pointer.'); end; destructor TCustomListViewColProperties.Destroy; begin inherited; FProperties.Free; end; procedure TCustomListViewColProperties.SetWidthsStr(Value: string; PixelsPerInch: Integer); var ColStr: string; Index: Integer; NeedInvalidate, NewVisible: Boolean; NewWidth: Integer; begin Index := 0; NeedInvalidate := False; BeginUpdate; try while (Value <> '') and (Index < Count) do begin ColStr := CutToChar(Value, ';', True); NewWidth := LoadDimension(StrToInt(CutToChar(ColStr, ',', True)), PixelsPerInch, FListView); Widths[Index] := NewWidth; NewVisible := Boolean(StrToInt(CutToChar(ColStr, ',', True))); if Visible[Index] <> NewVisible then begin // As we are within BeginUpdate guard, the width set just above is not propadated to WinAPI. // As reading width retrieves it from WinAPI (if the control is already allocated), we actually save wrong width. SetVisibleInternal(Index, NewVisible, False); NeedInvalidate := True; end; Inc(Index); end; finally EndUpdate; end; // When visibility changes (particularly while reseting layout) redraw is needed // (Invalidate is called in SetVisible too, but maybe it has no effect there, because it is within BeginUpdate/EndUpdate) if NeedInvalidate and FListView.HandleAllocated then FListView.Invalidate; end; function TCustomListViewColProperties.GetWidthsStr: string; var Index: Integer; begin Result := ''; for Index := 0 to Count-1 do begin Result := Format('%s;%d,%d', [Result, SaveDimension(Widths[Index]), Integer(Visible[Index])]); end; Delete(Result, 1, 1); end; procedure TCustomListViewColProperties.BeginUpdate; begin Columns.BeginUpdate; Inc(FUpdating); end; procedure TCustomListViewColProperties.EndUpdate; begin Columns.EndUpdate; Dec(FUpdating); if FUpdating = 0 then begin // call Changed() even when FChange is false Changed; FChanged := False; end; end; procedure TCustomListViewColProperties.Changed; begin if FUpdating > 0 then FChanged := True else if Assigned(FOnChange) then FOnChange(Self); end; procedure TCustomListViewColProperties.CheckBounds(Index: Integer); begin if (Index < 0) or (Index >= Count) then raise Exception.Create('Index out of bounds.'); end; function TCustomListViewColProperties.GetProperties(Index: Integer): TCustomListViewColProperty; begin Result := TCustomListViewColProperty(FProperties.Items[Index]); end; function TCustomListViewColProperties.GetIndexByOrder(Order: Integer): Integer; var I: Integer; begin for I := 0 to Count - 1 do begin if GetProperties(I).Order = Order then begin Result := I; Exit; end; end; raise Exception.Create('Column order out of bounds'); end; function TCustomListViewColProperties.ColumnsExists: Boolean; begin Result := FListView.HandleAllocated; if Result and (not FCreated) and (not FListViewManaged) then UpdateListView; end; procedure TCustomListViewColProperties.SetAlignments(Index: Integer; Value: TAlignment); begin CheckBounds(Index); if Alignments[Index] <> Value then begin GetProperties(Index).Alignment := Value; if ColumnsExists then GetColumn(Index).Alignment := Value; Changed; end; end; procedure TCustomListViewColProperties.SetCaptions(Index: Integer; Value: string); begin CheckBounds(Index); if Captions[Index] <> Value then begin if ColumnsExists then GetColumn(Index).Caption := Value else GetProperties(Index).Caption := Value; Changed; end; end; function TCustomListViewColProperties.GetAlignments(Index: Integer): TAlignment; begin CheckBounds(Index); if ColumnsExists then Result := GetColumn(Index).Alignment else Result := GetProperties(Index).Alignment; end; function TCustomListViewColProperties.GetCaptions(Index: Integer): string; begin CheckBounds(Index); if ColumnsExists then Result := GetColumn(Index).Caption else Result := GetProperties(Index).Caption; end; procedure TCustomListViewColProperties.SetOrderStr(Value: string); var Order, Index: Integer; Properties: TCustomListViewColProperty; STemp: string; Phase: Boolean; begin BeginUpdate; try for Index := 0 to Count - 1 do GetProperties(Index).Order := -1; // First order invisible columns (not True), then visible (not not True) Phase := True; Order := 0; repeat Phase := not Phase; STemp := Value; while (STemp <> '') and (Order < Count) do begin Index := StrToInt(CutToChar(STemp, ';', True)); Properties := GetProperties(Index); if (Properties.Visible = Phase) and (Properties.Order < 0) { robustness } then begin Properties.Order := Order; Inc(Order); end; end; // add missing columns from the same visibility class for Index := 0 to Count - 1 do begin Properties := GetProperties(Index); if (Properties.Visible = Phase) and (Properties.Order < 0) then begin Properties.Order := Order; Inc(Order); end; end; until Phase; if ColumnsExists then UpdateListViewOrder; finally EndUpdate; end; end; procedure TCustomListViewColProperties.SetParamsStr(Value: string); var S: string; WidthsStr: string; OrderStr: string; PixelsPerInch: Integer; begin // TFileFindDialog uses / as separator of its settings S := CutToChar(Value, '|', True); WidthsStr := CutToChar(S, '@', True); PixelsPerInch := LoadPixelsPerInch(S, FListView); SetWidthsStr(WidthsStr, PixelsPerInch); // Have to set order after visibility, otherwise we lost ordering of columns that are invisible by default, // but visible by configuration (as they would get ordered to the front) OrderStr := CutToChar(Value, '|', True); SetOrderStr(OrderStr); end; procedure TCustomListViewColProperties.SetVisibleInternal(Index: Integer; Value: Boolean; SaveWidth: Boolean); var I: Integer; Properties: TCustomListViewColProperty; begin CheckBounds(Index); if Visible[Index] <> Value then begin Properties := GetProperties(Index); if ColumnsExists then UpdateOrderFromListView; if Value then begin // shown column is moved to the back for I := 0 to Count - 1 do begin if GetProperties(I).Order > Properties.Order then Dec(GetProperties(I).Order); end; Properties.Order := Count - 1; if ColumnsExists then UpdateListViewOrder; // show only after reordering column Properties.Visible := True; if ColumnsExists then SetRuntimeVisible(Index, True, SaveWidth); end else begin // hide before reordering column Properties.Visible := False; if ColumnsExists then SetRuntimeVisible(Index, False, SaveWidth); // hidden column is moved to the front, // unless column to the left is not hidden already // (or unless it is first already, in which case the // condition in the loop is never satisfied) if (Properties.Order > 0) and GetProperties(GetIndexByOrder(Properties.Order - 1)).Visible then begin for I := 0 to Count - 1 do begin if GetProperties(I).Order < Properties.Order then Inc(GetProperties(I).Order); end; Properties.Order := 0; end; if ColumnsExists then UpdateListViewOrder; end; Changed; // It does not refresh itself at last one on Win7 when DoubleBuffered if FListView.HandleAllocated then FListView.Invalidate; end; end; procedure TCustomListViewColProperties.SetVisible(Index: Integer; Value: Boolean); begin SetVisibleInternal(Index, Value, True); end; procedure TCustomListViewColProperties.SetRuntimeVisible( Index: Integer; Value: Boolean; SaveWidth: Boolean); var Properties: TCustomListViewColProperty; begin // This is probably only ever called from file panels (DirViews) // as other uses ("sychronization checklist" and "file find") // have FListViewManaged = False and never change Visible property // (though user can hide some columns manually in configuration storage) with GetColumn(Index) do begin Properties := GetProperties(Index); if Value then begin MaxWidth := Properties.MaxWidth; MinWidth := Properties.MinWidth; Width := Properties.Width; end else begin if SaveWidth then Properties.Width := Width; MaxWidth := 1; MinWidth := 0; Width := 0 end; end; end; procedure TCustomListViewColProperties.SetWidths(Index: Integer; Value: Integer); var Properties: TCustomListViewColProperty; begin CheckBounds(Index); Properties := GetProperties(Index); if (Properties.MinWidth > 0) and (Value < Properties.MinWidth) then begin Value := Properties.MinWidth; end else if (Properties.MaxWidth > 0) and (Value > Properties.MaxWidth) then begin Value := Properties.MaxWidth; end; if Widths[Index] <> Value then begin Properties.Width := Value; if ColumnsExists and Visible[Index] then GetColumn(Index).Width := Value; Changed; end; end; function TCustomListViewColProperties.GetColumns: TListColumns; begin Result := TListView(FListView).Columns; end; function TCustomListViewColProperties.GetColumn(Index: Integer): TListColumn; begin Result := Columns[Index]; end; function TCustomListViewColProperties.GetCount: Integer; begin Result := FProperties.Count; end; function TCustomListViewColProperties.GetOrderStr: string; var Index: Integer; begin Result := ''; if ColumnsExists then UpdateOrderFromListView; for Index := 0 to Count - 1 do Result := Format('%s;%d', [Result, GetIndexByOrder(Index)]); Delete(Result, 1, 1); end; function TCustomListViewColProperties.GetParamsStr: string; begin // WORKAROUND // Adding an additional semicolon after the list, // to ensure that old versions that did not expect the pixels-per-inch part, // stop at the semicolon, otherwise they try to parse the // "last-column-width|pixels-per-inch" as integer and throw. // For the other instance of this hack, see GetListViewStr. // The new pixels-per-inch part is inserted after the widths part // as parsing of this was always robust to stop at "count" elements, // what order part was not (due to its logic of skipping hidden columns) Result := Format('%s;@%s|%s', [GetWidthsStr, SavePixelsPerInch(FListView), GetOrderStr]); end; function TCustomListViewColProperties.GetVisible(Index: Integer): Boolean; begin CheckBounds(Index); Result := GetProperties(Index).Visible; end; function TCustomListViewColProperties.GetWidths(Index: Integer): Integer; begin CheckBounds(Index); if ColumnsExists and Visible[Index] then begin Result := GetColumn(Index).Width; end else begin Result := GetProperties(Index).Width; end; end; procedure TCustomListViewColProperties.RecreateColumns; var Copy: TListColumns; begin Copy := TListColumns.Create(nil); try Copy.Assign(Columns); Columns.Assign(Copy); finally Copy.Free; end; end; procedure TCustomListViewColProperties.CreateProperties(ACount: Integer); var Index: Integer; Properties: TCustomListViewColProperty; begin for Index := 0 to ACount - 1 do begin Properties := TCustomListViewColProperty.Create(Index); FProperties.Add(Properties); end; end; function TCustomListViewColProperties.DefaultConstraint(Value: Integer; Visible: Boolean; Def: Integer): Integer; begin if (Value > 0) and Visible then Result := Value else Result := Def; end; procedure TCustomListViewColProperties.ListViewWndCreated; var Index: Integer; Properties: TCustomListViewColProperty; Column: TListColumn; begin if FListViewManaged then begin if (FProperties.Count = 0) and (Columns.Count > 0) then CreateProperties(Columns.Count); UpdateFromListView; end else begin UpdateListView; end; Assert(ColumnsExists); for Index := 0 to Count - 1 do begin Column := GetColumn(Index); Properties := GetProperties(Index); if not FConstraintsInitialized then begin Properties.MaxWidth := DefaultConstraint(Column.MaxWidth, Properties.Visible, DefaultListViewMaxWidth); Properties.MinWidth := DefaultConstraint(Column.MinWidth, Properties.Visible, DefaultListViewMinWidth); end; // To apply the default constraints to columns that do not have their own if Properties.Visible then begin Column.MaxWidth := Properties.MaxWidth; if Column.Width > Column.MaxWidth then Column.Width := Column.MaxWidth; Column.MinWidth := Properties.MinWidth; if Column.Width < Column.MinWidth then Column.Width := Column.MinWidth; end; end; FConstraintsInitialized := True; end; procedure TCustomListViewColProperties.ListViewWndDestroying; begin UpdateFromListView; end; procedure TCustomListViewColProperties.ListViewWndDestroyed; begin if not FListViewManaged then FCreated := False; end; procedure TCustomListViewColProperties.ChangeScale(M, D: Integer); var Index: Integer; Properties: TCustomListViewColProperty; Column: TListColumn; begin for Index := 0 to Count - 1 do begin Properties := GetProperties(Index); // This is not perfect, as the constraints apply on re-scaled width before they are re-scaled themselves if Properties.Visible and ColumnsExists then begin Column := GetColumn(Index); Column.MaxWidth := MulDiv(Column.MaxWidth, M, D); Column.MinWidth := MulDiv(Column.MinWidth, M, D); end else begin Properties.MaxWidth := MulDiv(Properties.MaxWidth, M, D); Properties.MinWidth := MulDiv(Properties.MinWidth, M, D); end; end; end; procedure TCustomListViewColProperties.UpdateListViewOrder; var Index: Integer; Properties: TCustomListViewColProperty; Temp: array of Integer; begin SetLength(Temp, Count); // Seemingly useless, // but probably only because we swallow HDN_ENDDRAG in TCustomIEListView.WMNotify, // what prevents VLC from actually reordering columns collection ListView_GetColumnOrderArray(FListView.Handle, Count, PInteger(Temp)); for Index := 0 to Count - 1 do begin Properties := GetProperties(Index); Temp[Properties.Order] := Index; end; ListView_SetColumnOrderArray(FListView.Handle, Count, PInteger(Temp)); end; procedure TCustomListViewColProperties.UpdateListView; var Index: Integer; Column: TListColumn; Properties: TCustomListViewColProperty; begin // Only called when FListViewManaged = False BeginUpdate; try for Index := 0 to Count-1 do begin if Index < Columns.Count then Column := GetColumn(Index) else Column := Columns.Add; Properties := GetProperties(Index); Column.Alignment := Properties.Alignment; Column.Caption := Properties.Caption; SetRuntimeVisible(Index, Properties.Visible, False); end; UpdateListViewOrder; finally FCreated := True; EndUpdate; end; end; procedure TCustomListViewColProperties.UpdateOrderFromListView; var Index: Integer; Temp: array of Integer; begin SetLength(Temp, Count); ListView_GetColumnOrderArray(FListView.Handle, Count, PInteger(Temp)); for Index := 0 to Count - 1 do begin GetProperties(Temp[Index]).Order := Index; end; end; procedure TCustomListViewColProperties.UpdateFromListView; var Index: Integer; Column: TListColumn; Properties: TCustomListViewColProperty; begin Assert(FProperties.Count = Columns.Count); for Index := 0 to Count-1 do begin Column := GetColumn(Index); Properties := GetProperties(Index); Properties.Alignment := Column.Alignment; Properties.Caption := Column.Caption; if Properties.Visible then begin Properties.Width := Column.Width; if Column.MaxWidth > 0 then Properties.MaxWidth := Column.MaxWidth; if Column.MinWidth > 0 then Properties.MinWidth := Column.MinWidth; end; end; UpdateOrderFromListView; end; end.