Delphi-PRAXiS
Seite 1 von 2  1 2      

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   GUI-Design mit VCL / FireMonkey / Common Controls (https://www.delphipraxis.net/18-gui-design-mit-vcl-firemonkey-common-controls/)
-   -   TNumberBox Min/Max-Eingabeproblem (https://www.delphipraxis.net/209337-tnumberbox-min-max-eingabeproblem.html)

idontknow 24. Nov 2021 08:24

TNumberBox Min/Max-Eingabeproblem
 
Moin,

ich steh gerade auf dem Schlauch: Ich würde gern eine TNumberbox zur Zahleneingabe verwenden und diese gern auf einen Zahlenraum von Min=5..Max=90 beschränken (Mal so als Beispiel).

Funktioniert im Prinzip wunderbar, solange ich nur die Up/Down-Buttons benutze.

Wenn ich nun aber direkt z.B. 45 eingeben möchte, führt schon die Eingabe der 4 dazu, das die untere Grenze verletzt wird und somit durch 5 ersetzt wird. Mit anderen Worten: Zahlen zwischen 10 und 49 kann ich so nicht direkt eingeben.

Mein Wunschverhalten wäre: Die Up/Down-Buttons sind begrenzt, es kann jedoch jede Zahl manuell eingegeben werden, hierbei werden die Grenzen z.B. erst bei OnExit geprüft und ggf. die Eingabe begrenzt.

Hat jemand eine Idee, wie ich das mit überschaubarem Aufwand realisieren kann?

Schon mal vielen Dank.

KodeZwerg 24. Nov 2021 08:36

AW: TNumberBox Min/Max-Eingabeproblem
 
Hallo, ich kenne diese Komponente noch nicht aber aus dem Bauch heraus würde ich eine eigene Prüfung einbinden.
Also min max so einstellen das alles angenommen werden kann.
Da ich nicht weiß wie deine GUI mit der Komponente aussieht und wann du dir erwartet hast das etwas passieren soll gebe ich hier ein mini beispiel.
Einen Knopf "Go." platzieren und dem OnClick davon dann deine Edit felder prüfen lassen, wenn alles gut ist "machWas()" wenn was aus der reihe hüpft "rotEinfärbenUndFocus()"
Ein automatische prüfung ohne Knopf, da würde ich mal gucken ob Komponente ein "OnLeave" oder "OnExit" hat um mich da reinzuklinken.

BerndS 24. Nov 2021 08:52

AW: TNumberBox Min/Max-Eingabeproblem
 
Ich habe so was ähnliches für ein Datumsfeld umgesetzt. Die Prüfung erfolgt aber nicht bei der Eingabe, sondern über eine Funktion der Eingabekomponente.
Die wird erst aufgerufen, wenn z.B. Speichern angeklickt wird oder auf eine andere Eingabezeile gewechselt wird. Man kann es auch im OnExit machen, sollte aber dann bei Modalresult = mrCancel die Prüfung übergehen.

Konkret habe ich eine Prüfung eingebaut, die beim MouseEnter auf den Speichernschalter im Hint das Ergebnis der Prüfung anzeigt und beim Klicken des Schalters eine Messagebox zeigt und die fehlerhafte Eingabezeile fokussiert.

idontknow 24. Nov 2021 09:27

AW: TNumberBox Min/Max-Eingabeproblem
 
Die Wertbegrenzung durch die Up/Down-Buttons würde ich ja gern erhalten.

Mir scheint die ganze Min/Max-Implementation sinnlos zu sein.

Um meine eigene Wert-Begrenzung einzubauen, müsste ich wissen, ob der Benutzer auf einen Up/Down-Button gedrückt hat.
Leider liefert die Komponente aber auch kein OnUpDownButtonPressed-Ereignis...

Da hilft wohl nur, eine eigene Komponente von TNumberBox abzuleiten und lange rumzufummeln, um diese Basisfunktionalität zu erreichen?

Uwe Raabe 24. Nov 2021 09:41

AW: TNumberBox Min/Max-Eingabeproblem
 
Das ist definitiv ein nicht zu erwartendes und auch nicht sinnvolles Verhalten. Bitte mach dafür doch einen QP-Report auf, damit das behoben werden kann. Die Min/Max-Überprüfung darf einfach nicht nach jedem Tastendruck erfolgen, sondern erst bei Enter oder Verlassen des Controls.

Als Workaround kannst du das AcceptExpressions einschalten und deine Eingabe mit einem + beginnen. Für die Praxis taugt das allerdings nicht.

dummzeuch 24. Nov 2021 10:02

AW: TNumberBox Min/Max-Eingabeproblem
 
Ich würde da die Eingabeprüfung generell erstmal ausschalten und sie verzögert durchführen lassen. Entweder erst, wenn der User OK (oder Save oder was auch immer) klickt, oder durch einen Timer gesteuert. Letzteres ist dabei meine bevorzugte Methode:

Es gibt einen Timer mit z.B. 200 ms Laufzeit. Der wird bei jedem OnChange-Event zuerst gestoppt und dann wieder gestartet, d.h. jeder Tastendruck startet die Zeit wieder von vorne. Im OnTimer-Event wird dann die Eingabeprüfung durchgeführt und falls sie fehlschägt dem User "irgendwie" mitgeteilt. Das "irgendwie" könnte z.B ein Label mit einer Fehlermeldung einblenden, das wieder verschwindet, wenn die Eingabe OK ist.

Wenn ich so drüber nachdenke: Eigentlich braucht man den Timer gar nicht. Man kann die Prüfung sofort im OnChange Event durchführen, solange diese Prüfung die Eingabe nicht stört. Der Timer hätte nur den Vorteil, dass der User nicht unnötig eine Fehlermeldung angezeigt bekommt, wenn er mit der Eingabe noch gar nicht fertig ist.

KodeZwerg 24. Nov 2021 10:13

AW: TNumberBox Min/Max-Eingabeproblem
 
Zitat:

Zitat von dummzeuch (Beitrag 1498096)
Wenn ich so drüber nachdenke: Eigentlich braucht man den Timer gar nicht. Man kann die Prüfung sofort im OnChange Event durchführen, solange diese Prüfung die Eingabe nicht stört. Der Timer hätte nur den Vorteil, dass der User nicht unnötig eine Fehlermeldung angezeigt bekommt, wenn er mit der Eingabe noch gar nicht fertig ist.

Aber genau das ist ja gerade das problem, woher soll code im OnChange nun wissen was legal (erfolgt noch mehr?...) und was es nicht ist (dies war der einzige input, ersetze eingabe mit min/max). An dieser Stelle ist eine Prüfung falsch.

DeddyH 24. Nov 2021 10:36

AW: TNumberBox Min/Max-Eingabeproblem
 
Das Problem ist doch nicht, dass eine "falsche" Eingabe erkannt wird, sondern dass diese dann automatisch "korrigiert" wird. Ein optisches Feedback, dass der aktuelle Wert nicht plausibel ist, halte ich auch für sinnvoll und praktiziere das auch öfter so, indem ein rotes Label eingeblendet und bei gültigem Wert wieder ausgeblendet wird.

dummzeuch 24. Nov 2021 10:36

AW: TNumberBox Min/Max-Eingabeproblem
 
Zitat:

Zitat von KodeZwerg (Beitrag 1498101)
Zitat:

Zitat von dummzeuch (Beitrag 1498096)
Wenn ich so drüber nachdenke: Eigentlich braucht man den Timer gar nicht. Man kann die Prüfung sofort im OnChange Event durchführen, solange diese Prüfung die Eingabe nicht stört. Der Timer hätte nur den Vorteil, dass der User nicht unnötig eine Fehlermeldung angezeigt bekommt, wenn er mit der Eingabe noch gar nicht fertig ist.

Aber genau das ist ja gerade das problem, woher soll code im OnChange nun wissen was legal (erfolgt noch mehr?...) und was es nicht ist (dies war der einzige input, ersetze eingabe mit min/max). An dieser Stelle ist eine Prüfung falsch.

Deshalb halt keine automatische Prüfung+Korrektur in der Komponente (also Min/Max ausschalten) und selbst implementieren. Dabei aber dem User nicht dazwischenpfuschen (Was ich persönlich als User absolut nicht abkann.).

idontknow 25. Nov 2021 15:46

AW: TNumberBox Min/Max-Eingabeproblem
 
Ich hab eine Helper-Klasse geschrieben, mit der das ganze nun für meine Anwendung gut funktioniert.

Falls jemand dasselbe Problem hat, einfach die Unit einbinden und im FormCreate eine Zeile hinzufügen:

Delphi-Quellcode:
procedure TForm1.FormCreate(Sender: TObject);
begin
  TNumberBoxHelper.InitializeNumberBoxes(Self); // rekursiv alle TNumberBox'en auf den Class Helper einschwören
end;

Delphi-Quellcode:
unit lib_NumberBoxHelper;

// Eine kleine Helper-Klasse für TNumberBox.
// 25.11.2021, idontknow
//
// TNumberBox lässt normalerweise keine Eingabe von Ziffern zu, sobald diese einzeln betrachtet bereits die Min/Max-Grenzen
// verletzen. Eine NumberBox mit Min=5 und Max=90 wird den Wert 45 nicht akzeptieren wenn dieser eingetippt wird, weil bereits
// die Ziffer 4 die untere Grenze verletzt.
//
// Diese Helper-Klasse legt bei Aufruf von TNumberBox.Init ein TNumberBoxHelper-Objekt an.
// TNumberBox.Min und Max werden daraufhin auf 0 gesetzt, TNumberBox nimmt somit jeden Wert an.
// Bei OnExit oder nach Ablauf eines Timers, der gestartet wird, sobald die bisherige Eingabe die Min/Max-Bedingung nicht erfüllt,
// wird TNumberBox.Value korrigiert.
//
// Beispiel für Benutzung, die Numberboxen seien mit OnChange=NumberBoxValueChanged auf dem Form festgelegt:
//
// procedure TForm1.FormCreate(Sender: TObject);
// begin
//   TNumberBoxHelper.InitializeNumberBoxes(Self); // rekursiv alle TNumberBox'en auf den Class Helper einschwören
// end;
//
// procedure TForm1.NumberBoxValueChanged(Sender: TObject);
// var
//   NumberBox: TNumberBox;
//   Text: String;
// begin
//   NumberBox := TNumberBox(Sender);
//
//   if NumberBox.Mode = nbmInteger then
//     Text := Format('NumberBox: %s, LastValue: %d, Value: %d', [NumberBox.Name, NumberBox.PreviousValueInt, NumberBox.ValueInt]);
//
//   if NumberBox.Mode = nbmFloat then
//     Text := Format('NumberBox: %s, LastValue: %.2f, Value: %.2f', [NumberBox.Name, NumberBox.PreviousValueFloat, NumberBox.ValueFloat]);
//
//   Memo1.Lines.Add(Text);
// end;
//
// Achtung: TNumberBox.Tag (von TControl geerbt) brauche ich blöderweise in meiner Application auch.
// Hier wird Tag verwendet (weil ein class helper keine neuen Felder haben kann), um einen Zeiger auf das TNumberBoxHelper-Objekt
// zu haben.
// Lösung: Ich verwende überall TControl(Self).Tag zum Zugriff auf den "TNumberBoxHelper-Objekt-Zeiger"
// und ein neu eingeführtes Property TNumberBoxHelperClass.Tag zum Zugriff auf TNumberBoxHelper.Tag.
// Nach aussen gibt es somit weiterhin ein frei verwendbares Tag-Property.

interface

uses
  System.Classes, Vcl.Controls, Vcl.NumberBox, Vcl.ExtCtrls, System.Math;

const
  DetermineUpDownIntervalms = 100;

type
  TNumberBoxHelper = class(TControl) // damit die Objekte automatisch aufgeräumt werden...
  private
    MinValue, MaxValue, Value, PreviousValue: Extended; // Value und PreviousValue sind gültige Werte
    Input: Extended;                                   // Input kann eine ungültige Eingabe ausserhalb des MinMaxRange sein
    Tag: NativeUint;
    Changed: Boolean;
    LastKeyPressedAt: Int64;
    onChange: TNotifyEvent;
    onEnter: TNotifyEvent;
    onExit: TNotifyEvent;
  public
    class procedure InitializeNumberBoxes(WinControl: TWinControl);
  end;

  TNumberBoxHelperClass = class helper for TNumberBox
  private
    // NumberBoxHelper: TNumberBoxHelper; leider keine Felder in class helper, daher verwende ich TNumberBox.Tag
    class var ValidationTimer: TTimer;
    procedure ValidationTimerElapsed(Sender: TObject);
    procedure CreateValidationTimer;
    function GetMaxValue: Extended;
    function GetMinValue: Extended;
    function GetTag: NativeUInt; // Beschafft Tag aus TNumberBoxHelper, darin steht das ursprüngliche TNumberBox.Tag
    procedure SetMaxValue(const Value: Extended);
    procedure SetMinValue(const Value: Extended);
    procedure SetTag(const Value: NativeUInt);
    procedure DoValidateChar(AChar: Char; var AValidated: Boolean);
  public
    procedure Init;
    procedure DoEnterOrChange(Sender: TObject);
    procedure DoChange(Sender: TObject);
    procedure DoEnter(Sender: TObject);
    procedure DoExit(Sender: TObject);
    function Correct: Boolean;
    function PreviousValueInt: Integer;
    function PreviousValueFloat: Extended;
    class procedure ForceCorrection;
  published
    property Tag: NativeUInt read GetTag write SetTag;
    property MinValue: Extended read GetMinValue write SetMinValue;
    property MaxValue: Extended read GetMaxValue write SetMaxValue;
  end;

implementation

class procedure TNumberBoxHelper.InitializeNumberBoxes(WinControl: TWinControl);
var
  i: Integer;
  Control: TControl;
begin
  for i := 0 to WinControl.ControlCount-1 do
  begin
    Control := WinControl.Controls[i];
    if Control.InheritsFrom(TNumberBox) then
      TNumberBox(Control).Init
    else
      if Control.InheritsFrom(TWinControl) then
        InitializeNumberBoxes(TWinControl(Control));
  end;
end;

{ TNumberBoxHelperClass }
procedure TNumberBoxHelperClass.DoValidateChar(AChar: Char; var AValidated: Boolean);
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  // Das ist hier der Weg rauszufinden, ober der User eine Zahl eintippt oder die Up/Down-Buttons verwendet...
  AValidated := TRUE;
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  NumberBoxHelper.LastKeyPressedAt := Int64(TThread.GetTickCount64);
end;

procedure TNumberBoxHelperClass.DoExit(Sender: TObject);
var
  NumberBox: TNumberBox;
begin
  if not Assigned(ValidationTimer) then
    CreateValidationTimer;

  ValidationTimer.Tag := 0;
  ValidationTimer.Enabled := FALSE;

  NumberBox := TNumberBox(Sender);
  NumberBox.Correct;
end;

procedure TNumberBoxHelperClass.CreateValidationTimer;
begin
  ValidationTimer := TTimer.Create(Self);
  ValidationTimer.OnTimer := ValidationTimerElapsed;
  ValidationTimer.Tag := 0;
  ValidationTimer.Enabled := FALSE;
end;

procedure TNumberBoxHelperClass.DoChange(Sender: TObject);
begin
  DoEnterOrChange(Sender);
end;

procedure TNumberBoxHelperClass.DoEnter(Sender: TObject);
var
  NumberBox: TNumberBox;
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBox := TNumberBox(Sender);
  NumberBoxHelper := TNumberBoxHelper(TControl(NumberBox).Tag);
  if Assigned(NumberBoxHelper.onEnter) then
    NumberBoxHelper.onEnter(Sender);
  DoEnterOrChange(Sender);
end;

procedure TNumberBoxHelperClass.DoEnterOrChange(Sender: TObject);
var
  NumberBox: TNumberBox;
  NumberBoxHelper: TNumberBoxHelper;
begin
  if not Assigned(ValidationTimer) then
    CreateValidationTimer;

  ValidationTimer.Enabled := FALSE;
  ValidationTimer.Tag := NativeUInt(Sender);

  NumberBox := TNumberBox(Sender);
  NumberBoxHelper := TNumberBoxHelper(TControl(NumberBox).Tag);
  if SameValue(Abs(NumberBox.Value - NumberBoxHelper.Value), NumberBox.SmallStep)
  or SameValue(Abs(NumberBox.Value - NumberBoxHelper.Input), NumberBox.SmallStep) then
  begin
    // wenn der Unterschied +- SmallStep beträgt UND die letzte Tastatureingabe in das Eingabefeld länger als ~100ms her ist,
    // dann wurde mit hoher Wahrscheinlichkeit eine Up/Down-Taste gedrückt -> Wert sofort korrigieren
    if ((Int64(TThread.GetTickCount64) - NumberBoxHelper.LastKeyPressedAt) > DetermineUpDownIntervalms) then
    begin
      NumberBox.Correct;
      NumberBoxHelper.Input := NumberBox.Value;
    end;
  end
  else begin
    NumberBoxHelper.Input := NumberBox.Value;
    // speichert jede (auch ungültige) Eingabe, die vielleicht gleich durch Up/Down inkrementiert/dekrementiert wird.
    ValidationTimer.Enabled := TRUE;
  end;
end;

function TNumberBoxHelperClass.Correct: Boolean;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);

  if Value < NumberBoxHelper.MinValue then
    Value := NumberBoxHelper.MinValue
  else
  if Value > NumberBoxHelper.MaxValue then
    Value := NumberBoxHelper.MaxValue;

  Result := NumberBoxHelper.Value <> Value;
  NumberBoxHelper.Changed := NumberBoxHelper.Changed or Result;
  NumberBoxHelper.Value := Value;

  if NumberBoxHelper.Changed then
  begin
    if Assigned(NumberBoxHelper.onChange) then
      NumberBoxHelper.onChange(Self);

    NumberBoxHelper.PreviousValue := NumberBoxHelper.Value;

    NumberBoxHelper.Changed := FALSE;
  end;
end;

procedure TNumberBoxHelperClass.ValidationTimerElapsed(Sender: TObject);
begin
  ForceCorrection;
end;

class procedure TNumberBoxHelperClass.ForceCorrection;
var
  NumberBox: TNumberBox;
begin
  // wird von ValidationTimerElapsed aufgerufen oder kann vom Benutzer aufgerufen werden.
  // Vom Benutzer normalerweise nicht nötig, weil onExit ebenfalls ForceCorrection aufruft.
  if ValidationTimer.Enabled then
  begin
    ValidationTimer.Enabled := FALSE;
    NumberBox := TNumberbox(ValidationTimer.Tag);
    NumberBox.Correct;
  end;
end;

procedure TNumberBoxHelperClass.Init;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper.Create(Self);

  // Die Min/Max-Werte im NumberBoxHelper ablegen...
  NumberBoxHelper.MinValue := TCustomNumberBox(Self).MinValue;
  NumberBoxHelper.MaxValue := TCustomNumberBox(Self).MaxValue;

  NumberBoxHelper.Value := Value;
  NumberBoxHelper.PreviousValue := Value;
  NumberBoxHelper.Input := Value;
  NumberBoxHelper.Changed := FALSE;

  NumberBoxHelper.onEnter := OnEnter;
  NumberBoxHelper.onExit := OnExit;
  NumberBoxHelper.onChange := OnChange;

  Self.onValidateChar := DoValidateChar;

  NumberBoxHelper.Tag := TControl(Self).Tag;          // Tag in Sicherheit bringen...
  TControl(Self).Tag := NativeUint(NumberBoxHelper);  // und ab jetzt intern verwenden... Das Ursprungs-Tag mit TNumberBox.getTag ermitteln.

  OnEnter := nil;
  OnChange := nil;
  OnExit := nil;

  // ... und hier auf 0 setzen, damit künftig jeder Wert bei der Eingabe akzeptiert wird
  TCustomNumberBox(Self).MinValue := 0;
  TCustomNumberBox(Self).MaxValue := 0;

  NumberBoxHelper.LastKeyPressedAt := Int64(TThread.GetTickCount64) - 1000;
  Correct; // Den Wert ggf. schon mal in die Begrenzung fahren, jedoch ohne OnChange.

  OnEnter := DoEnter;
  OnChange := DoChange;
  OnExit := DoExit;
end;

function TNumberBoxHelperClass.PreviousValueInt: Integer;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  Result := Round(NumberBoxHelper.PreviousValue);
end;

function TNumberBoxHelperClass.PreviousValueFloat: Extended;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  Result := NumberBoxHelper.PreviousValue;
end;

procedure TNumberBoxHelperClass.SetMaxValue(const Value: Extended);
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  NumberBoxHelper.MaxValue := Value;
end;

procedure TNumberBoxHelperClass.SetMinValue(const Value: Extended);
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  NumberBoxHelper.MinValue := Value;
end;

procedure TNumberBoxHelperClass.SetTag(const Value: NativeUInt);
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  NumberBoxHelper.Tag := Value;
end;

function TNumberBoxHelperClass.GetMaxValue: Extended;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  Result := NumberBoxHelper.MaxValue;
end;

function TNumberBoxHelperClass.GetMinValue: Extended;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  Result := NumberBoxHelper.MinValue;
end;

function TNumberBoxHelperClass.GetTag: NativeUInt;
var
  NumberBoxHelper: TNumberBoxHelper;
begin
  // weil wir TNumberBox.Tag zum Speichern des NumberBoxHelper verwenden...
  NumberBoxHelper := TNumberBoxHelper(TControl(Self).Tag);
  // gibt es stattdessen ein Tag-Feld im NumberBoxHelper...
  Result := NumberBoxHelper.Tag;
end;

end.


Alle Zeitangaben in WEZ +1. Es ist jetzt 05:47 Uhr.
Seite 1 von 2  1 2      

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz