hi!
ich habe mir ein scheduler-control gebastelt - im anhang ist ein screenshot.
das gesamte control wird - auf einzelne methoden aufgesplittet - im OnPaint gezeichnet.
man kann die termine natürlich anklicken, verschieben, vergrößern/verkleinern.
ebenso kann man die gesamte fläche scrollen, entweder per mausrad oder einfach auf den hintergrund klicken und diesen nach oben/unten schieben (im hintergrund werkelt ein scroll-balken.
dabei wird einfach gesagt im OnMouseMove-Event der value des scroll-balken verändert und Invalidate() ausgeführt.
das gleiche trifft für das verschieben/vergrößern/verkleinern von terminen zu.
hier wird die neue größe/position berechnet und wieder ein Invalidate() ausgeführt.
problem: bei fullscreen (1080p) und einem schwachen prozessor (ich habe zu testzwecken meinen i5 mal auf 2,5ghz gesenkt) ruckelt das verschieben/scrollen dann merklich.
bei den terminen könnte ich mir nun vorstellen, dass man immer nur den bereich des termins neu zeichnet und nicht das ganze control.
aber wenn ich die zeitleiste verschiebe, MUSS ich ja zwangsweise >90% des controls neu zeichnen.
kann ich das ruckeln irgendwie vermeiden?
und... warum ruckelt das egtl.? ich meine .. es ist ein i5. wenn ich mir jetzt irgendein spiel vorstelle, das selbst auf einem athlon xp 3500+ gelaufen ist, z.B. quake iii, was ist da schon dieses control dagegen? 😃
wäre sehr dankbar für ideen.
der outlook scheduler ruckelt ja auch nicht 😦
[EDIT]
sry, screenshot vergessen 😃
Hallo uNki,
hast du schon alles aus [FAQ] Flackernde Controls vermeiden / Schnelles, flackerfreies Zeichnen und aus den verlinkten Artikeln probiert?
herbivore
womöglich habe ich ein grundsätzliches problem, da ich nicht mit objekten arbeite (wie in deinen links), die ich zu jedem zeitpunkt der runtime benennen/identifizieren könnte. außer die termine, aber das sind keine grafik-objekte sondern nur objekte, die eine start- und endzeit sowie eine farbe enthalten. in einer methode DrawAppointment() erstelle ich dann daraus das rechteck.
ich könnte also auch keine einzelnen objekte gezielt zeichnen oder ansprechen.
vielmehr habe ich das teil so aufgestellt, dass bei einer aktion, z.b. scrollen des zeitfensters oder verschieben eines termins, alles neu zeichne, damit es am ende so aussieht, wie es eben aussehen soll.
woher weiß ich, welches objekt unter der maus ist:
habe eine funktion, die mir ein DateTime je mausposition im kalenderbereich zurückgibt. mit dieser uhrzeit kann ich meine liste von termin-objekten durchlaufen und das unter der maus identifizieren oder bewegen (indem ich dessen start- und endzeit verändere und es neu zeichne).
ansonsten frage ich im mouseMove koordinaten ab und kann so identifizieren wo ich mich gerade aufhalte, z.b. in der zeitleiste, oder einem der beiden knöpfe oben links.
mit g.SetClip() und g.ResetClip() arbeite ich, aber im fullscreen muss beim scrollen des controls meiner ansicht nach einfach ALLES neu gezeichnet werden, da sich ja ALLES ändert.
hier mal mein OnPaint()-event und eine beispiel methode, die in diesem fall einen tag zeichnet, also den weißen und blauen hintergrund, samt zeitlinien.
dann habt ihr einen eindruck davon, wie das control bei mir erstellt wird und seht evtl. ein grundsätzliches problem in meiner umsetzung.
wie gesagt, wenn gescrollt, verschoben, vergrößert, verkleinert wird, rufe ich jedes mal Invalidate() auf.
protected override void OnPaint(PaintEventArgs e)
{
txtSearch.Location = new Point(this.Width - 200, 10);
//hintergrund
using (SolidBrush backBrush = new SolidBrush(Color.White))
e.Graphics.FillRectangle(backBrush, this.ClientRectangle);
//sichtbare Fläche
Rectangle rectangle;
if (AllowScroll == true)
{
rectangle = new Rectangle(0, 0, this.Width - scrollbar.Width, this.Height);
}
else
{
rectangle = new Rectangle(0, 0, this.Width, this.Height);
}
//tage zeichnen
Rectangle daysRectangle = rectangle;
daysRectangle.X += hourLabelWidth + hourLabelIndent;
daysRectangle.Y += headerHeight + parkingAppContainerHeight;
daysRectangle.Width -= (hourLabelWidth + hourLabelIndent);
DrawDays(e, daysRectangle); //--> ruft je tag "DrawDay()" auf
//zeitleiste background
Rectangle hourLabelBackgroundRect = rectangle;
hourLabelBackgroundRect.Width = hourLabelWidth + hourLabelIndent;
hourLabelBackgroundRect.Y += headerHeight + parkingAppContainerHeight;
DrawHourLabelBackground(e.Graphics, hourLabelBackgroundRect);
//zeitleiste
Rectangle hourLabelRectangle = rectangle;
hourLabelRectangle.Y += headerHeight + parkingAppContainerHeight;
DrawHourLabels(e, hourLabelRectangle);
//appointments
//------------------------------------------------------
Point startPoint;
Point endPoint;
//erstmalig position der termine berechnen
if (calculatedAppointmentPositionsFirstTime == true)
{
CalculateAppointmentPositions();
calculatedAppointmentPositionsFirstTime = false;
}
if (activeApp != null && (activeApp.Moving == true || activeApp.SizeChanging == true))
{
foreach (cls_Appointment app in AppointmentList)
{
if (app != activeApp)
{
startPoint = GetPositionAtTime(app.Start);
endPoint = GetPositionAtTime(app.End);
DrawAppointment(e, app, startPoint, endPoint, app.Selected, app.Moving, app.SizeChanging, app.Start, app.End, app.ConflictDivider, app.XFactor);
}
}
if (activeApp.Parking == false)
{
startPoint = GetPositionAtTime(activeApp.Start);
endPoint = GetPositionAtTime(activeApp.End);
DrawAppointment(e, activeApp, startPoint, endPoint, activeApp.Selected, activeApp.Moving, activeApp.SizeChanging, activeApp.Start, activeApp.End, 1, 0);
}
}
else
{
foreach (cls_Appointment app in AppointmentList)
{
startPoint = GetPositionAtTime(app.Start);
endPoint = GetPositionAtTime(app.End);
DrawAppointment(e, app, startPoint, endPoint, app.Selected, app.Moving, app.SizeChanging, app.Start, app.End, app.ConflictDivider, app.XFactor);
}
}
//------------------------------------------------------
//header
Rectangle headerRectangle = rectangle;
headerRectangle.X += hourLabelWidth + hourLabelIndent;
headerRectangle.Width -= (hourLabelWidth + hourLabelIndent);
headerRectangle.Height = headerHeight;
DrawDayHeaders(e, headerRectangle);
//parkingAppContainer
Rectangle parkingAppContainerRectangle = rectangle;
parkingAppContainerRectangle.X += hourLabelWidth + hourLabelIndent;
parkingAppContainerRectangle.Y += headerHeight;
parkingAppContainerRectangle.Width -= (hourLabelWidth + hourLabelIndent);
parkingAppContainerRectangle.Height = parkingAppContainerHeight;
DrawParkingAppContainer(e, parkingAppContainerRectangle);
//parking apps zeichnen
DrawParkingAppointment(e);
//rahmen um das gesamte control
Rectangle topWhiteRectanle = new Rectangle(0, 0, this.Width, headerHeight-20);
using (SolidBrush brush = new SolidBrush(Color.White))
{
e.Graphics.FillRectangle(brush,topWhiteRectanle);
}
Rectangle borderRect = new Rectangle(0,0,this.Width-1,this.Height-1);
using (Pen borderPen = new Pen(Color.FromArgb(255, 167, 172, 176)))
{
e.Graphics.DrawRectangle(borderPen, borderRect);
}
using (Pen borderPen = new Pen(Color.FromArgb(255, 165,191,224)))
{
e.Graphics.DrawLine(borderPen, new Point(hourLabelWidth + hourLabelIndent, headerHeight), new Point(hourLabelWidth + hourLabelIndent, headerHeight+parkingAppContainerHeight));
e.Graphics.DrawLine(borderPen, new Point(0, headerHeight + parkingAppContainerHeight), new Point(this.Width, headerHeight + parkingAppContainerHeight));
}
//dreieckiger button "next week"
rectangle.X = 29;
rectangle.Y = 10;
rectangle.Width = 20;
rectangle.Height = 20;
if (rightArrowHover)
{
DrawArrow(e.Graphics, rectangle, false, true, false);
if (rightArrowPressed)
DrawArrow(e.Graphics, rectangle, false, true, true);
}
else
DrawArrow(e.Graphics, rectangle, false, false, false);
//dreieckiger button "prev week"
rectangle.X = 4;
rectangle.Y = 10;
rectangle.Width = 20;
rectangle.Height = 20;
if (leftArrowHover)
{
DrawArrow(e.Graphics, rectangle, true, true, false);
if (leftArrowPressed)
DrawArrow(e.Graphics, rectangle, true, true, true);
}
else
DrawArrow(e.Graphics, rectangle, true, false, false);
//string angezeigter zeitraum
using (Font fntDay = new Font("Segoe UI", 15, FontStyle.Regular, GraphicsUnit.Point))
{
rectangle.X = 65;
rectangle.Y = 5;
rectangle.Width = 500;
rectangle.Height = 30;
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
if (DaysToShow == 1)
e.Graphics.DrawString(StartDate.Day.ToString() + ". " + Monate[StartDate.Month - 1] + " " + StartDate.Year, fntDay, new SolidBrush(Color.FromArgb(255, 70, 70, 70)), rectangle);
else
{
if (StartDate.Month == StartDate.AddDays(DaysToShow).Month)
e.Graphics.DrawString(StartDate.Day.ToString() + ". - " + StartDate.AddDays(DaysToShow - 1).Day.ToString() + ". " + Monate[StartDate.Month - 1] + " " + StartDate.Year, fntDay, new SolidBrush(Color.FromArgb(255, 70, 70, 70)), rectangle);
else
e.Graphics.DrawString(StartDate.Day.ToString() + ". " + Monate[StartDate.Month - 1] + " " + StartDate.Year + " - " + StartDate.AddDays(DaysToShow - 1).Day.ToString() + ". " + Monate[StartDate.AddDays(DaysToShow - 1).Month - 1] + " " + StartDate.AddDays(DaysToShow - 1).Year, fntDay, new SolidBrush(Color.FromArgb(255, 70, 70, 70)), rectangle);
}
}
}
private void DrawDay(PaintEventArgs e, Rectangle rect, DateTime time)
{
//blauer hintergrund
using (SolidBrush backBrush = new SolidBrush(Color.FromArgb(230, 237, 247)))
e.Graphics.FillRectangle(backBrush, rect);
//weißer hintergrund für öffnungszeiten
int dayIndex = (int)(time.DayOfWeek);
DateTime workStart = DateTime.Parse(_workingHourList[dayIndex][0]);
DateTime workEnd = DateTime.Parse(_workingHourList[dayIndex][1]);
Rectangle workingHoursRectangle = GetHourRangeRectangle(workStart, workEnd, rect);
DrawHourRange(e.Graphics, workingHoursRectangle, false);
//auswahlrechteck
if (time.Date == appointmentSelectionStart.Date && selectionMade == true)
{
Rectangle selectionRectangle = GetHourRangeRectangle(appointmentSelectionStart, appointmentSelectionEnd, rect);
DrawHourRange(e.Graphics, selectionRectangle, true);
}
e.Graphics.SetClip(rect);
Color color1 = Color.FromArgb(165, 191, 225);
Color color2 = Color.FromArgb(213, 215, 241);
//trennlinien im abstand halbe stunde mit 2 versch. farben einfügen
for (int hour = 0; hour < 24 * slotsPerHour; hour++)
{
int y = rect.Top + (hour * _quarterHourHeight) - scrollbar.Value;
if (hour % slotsPerHour == 0)
{
using (Pen pen = new Pen(color1))
{
e.Graphics.DrawLine(pen, rect.Left, y, rect.Right, y);
}
}
else
{
using (Pen pen = new Pen(color2))
{
pen.DashStyle = DashStyle.Dash;
e.Graphics.DrawLine(pen, rect.Left, y, rect.Right, y);
}
}
if (y > rect.Bottom)
break;
}
//vertikale trennlinien zwischen einzelnen tagen
Pen borderPen = new Pen(color2);
e.Graphics.DrawRectangle(borderPen, rect);
////kleine leiste zur kategorisierung
//using (Brush brush = new SolidBrush(Color.White))
// e.Graphics.FillRectangle(brush, rect.Left, rect.Top - 1, 8, rect.Height);
//using (Pen pen = new Pen(Color.Black))
// e.Graphics.DrawRectangle(pen, rect.Left, rect.Top - 1, 8, rect.Height);
if (time.Date == DateTime.Today.Date || (time.Date == DateTime.Today.Date.AddDays(1).Date && time.Date != StartDate.Date))
using (Pen outlinePen = new Pen(Color.FromArgb(255, 244, 197, 59)))
{
//todayRect.Width -= 5;
e.Graphics.DrawRectangle(outlinePen, rect);
}
e.Graphics.ResetClip();
}
Hallo uNki,
das beantwortet allerdings meine Frage nicht. In der FAQ und den verlinkten Artikeln wird auch auf den Fall eingegangen, dass alles selber gezeichnet wird.
herbivore
ich habe die links angeschaut, dabei bin ich direkt auf
Der Grundgedanke ist einfach: Jedes zu zeichnende** Objekt (bei mir heißt die Klasse "Figure")** muß wissen, welchen Bereich es braucht, um sich zu zeichnen.
gestoßen und habe dann ja die frage gestellt, ob ich dann mit meinem control ein generelles problem habe, da ich keine grafik-objekte in irgendeiner collection habe, die ich durchlaufen kann um abzufragen "musst du dich neu zeichnen?".
weiterhin habe ich ja das problem, dass ich beim scrollen des gesamten kalenders (es ist ja nicht auf einen blick alles von 0-24uhr sichtbar) >90% neu zeichnen MUSS, da sich ja alles bewegt. oder sehe ich da etwas falsch?
und das ist ja genau der punkt, an dem es ruckelt.
außerdem erklärt sich mir noch immer nicht, warum es ÜBERHAUPT ruckeln kann. ich meine, es müssen dort ein paar rechtecke und linien gezeichnet werden. in summe sind das WENN ÜBERHAUPT vielleicht >100 rechtecke. das sollte doch überhaupt kein problem sein, so etwas mit 30fps zu zeichnen, auch wenn ich stets das gesamte control neu zeichnen will?!
es wird auch deutlich: je größer das control ist, desto mehr ruckelt es. in 1080p ist es halt schon deutlich spürbar, in 720p nicht. (bei 2ghz getaktetem i5).
[EDIT]
gibt es hier evtl. einen anderen weg den ich gehen sollte?
evtl. directx, statt gdi? da ich mich mit directx noch nicht beschäftigt habe, stelle ich direkt die frage: lässt sich das vorhandene projekt mit annehmbaren aufwand auf directx umstellen?
[EDIT2]
habe gerade gemerkt, dass die beispieldatei in dem thread
"Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte
ab einer gewissen menge von objekten auch ruckelt.
auf einem intel atom ist sowas doch gar nicht mehr flüssig darstellbar.
und mein scheduler control hat wesentlich mehr objekte zu zeichnen, als die beispieldatei mit den bunten förmchen.
ist gdi+ bei dem scheduler die falsche wahl? wie machen es dann aber die kommerziellen anbieter mit ihren winforms schedulern?
--> habe es gerade mal getestet mit radcontrols scheduler. die sind nicht scrollbar, haben keine unterteilungen in viertel-stunden, lassen minutengenaues verschieben der termine nicht zu und haben die gleiche cpu-last beim bewegen von objekten. die können es wohl also auch nicht besser.
trotzdem keine lösung für mich, das ding muss flüssiger werden 😦
Hallo uNki,
für mich geht alles was du schreibst, an der Sache vorbei. Natürlich kann man versuchen, vor dem Zeichnen zu ermitteln, ob die Rechtecke überhaupt sichtbar sind und sie dann gar nicht erst zeichnen. Aber wie du selbst sagst, wir das bei üblicherweise zweistelligen Zahlen an zu zeichnenden Rechtecken kaum einen Unterschied machen.
Ob du dagegen die naheliegenste Möglichkeit, double buffer einzuschalten, und ähnlich elementare Tipps ausprobiert hast, schreibst du nicht.
herbivore
Und noch ein Hinweis:
die GDI-Objekte (wie Font, Brush, Pen etc.) solltest du nicht bei jedem Zeichnen wieder neu anlegen (und disposen), da diese ja konstant sind und es auch einige Zeit benötigt.
Merke dir diese am besten einmalig in 'static readonly' Member deiner Klasse.
merry X-mas
Ob du dagegen die naheliegenste Möglichkeit, double buffer einzuschalten, und ähnlich elementare Tipps ausprobiert hast, schreibst du nicht.
ich habe
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.Selectable, true);
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
im konstruktor stehen.
was wären denn "ähnlich elementare Tipps", die die performance beim refreshen des gesamten controls anheben können?
aus den links entnehme ich nur sachen wie "double buffering!", "nutze g.SetClip()!", "zeichne nur, was du wirklich brauchst!".
das mache ich alles, bzw. muss ich ja wie gesagt teilweise einfach ALLES neu zeichnen.
die GDI-Objekte (wie Font, Brush, Pen etc.) solltest du nicht bei jedem Zeichnen wieder neu anlegen (und disposen), da diese ja konstant sind und es auch einige Zeit benötigt.
Merke dir diese am besten einmalig in 'static readonly' Member deiner Klasse.
habe ich in den links auch gelesen, das werde ich direkt umsetzen. ich glaube aber noch nicht wirklich dran, dass es daran liegt. wie gesagt: das control ruckelt erst, wenn es sehr groß ist. die generierung der gdi-objekte bleibt davon ja unberührt. aber ich werde es testen
Hallo uNki,
versuche mal zu ermitteln, welcher Teil der Zeichenroutine die meiste Zeit verbraucht. Kommentiere dazu einzelne Teile aus und schau, obs dann nicht mehr ruckelt. Geht ein bisschen in die Richtung: [Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden. Wenn du die einzelnen Teile in einzelne Methoden packst, könntest du mit einem Profiler ermitteln, welche der Methoden die meiste Zeit verbraucht.
herbivore
dabei wird einfach gesagt im OnMouseMove-Event der value des scroll-balken verändert und Invalidate() ausgeführt.
Lass' mal das Invalidate weg.
Du brauchst ja nicht alles neu zu zeichnen, wenn du scrollst.
Probiere mal das Projekt im Anhang aus - ruckelt das bei dir?
Ein paar Infos zu der Demo:
Wenn nein, setzte ich mal dran, den Code dahinter als Komponente zu veröffentlichen 🙂
Wenn du die einzelnen Teile in einzelne Methoden packst, könntest du mit einem Profiler ermitteln, welche der Methoden die meiste Zeit verbraucht.
ich habe egtl. fast alles in separaten methoden ausgelagert, kannst du mir einen profiler empfehlen, der die messungen durchführen kann?
Lass' mal das Invalidate weg.
Du brauchst ja nicht alles neu zu zeichnen, wenn du scrollst.
wüsste gerade nicht wie das gehen sollte
Probiere mal das Projekt im Anhang aus - ruckelt das bei dir?
nein das ruckelt nicht. by the way, was ist das? 😃
ich habe das projekt jetzt einfach mal als anhang beigelegt.
evtl. könnt ihr euch ja mal den code anschauen und vielleicht fällt euch ja dann sofort auf, wo das performance-problem herrührt.
und im gegenzug habt ihr einen ganz hübschen scheduler for free, der ja sicher ausbaufähig ist 😃
es ist natürlich noch viel zusammengeschustert, aber das ist ja auch kein produktives projekt, ist ja alles erstmal zum testen.
die Appointments.mdb muss in C:\ liegen, oder ihr ändert den connectionstring in der dataclass ab.
grüße!
nebenbei: schöne feiertage!
und im gegenzug habt ihr einen ganz hübschen scheduler for free, der ja sicher ausbaufähig ist 😃
Ist ja nicht so, dass es bisher keine Scheduler Controls geben würde 😉 Und einige sind sogar wartbarer als deiner. z.B. hab ich mal einen von Codeproject benutzt (will ihn grad nicht raussuchen), da konnte man einfach eigene Styles implementieren und ein paar waren schon vorimplementiert. Bei dir sind sogar teilweise Farben, Schriften usw. hartkodiert im Quelltext. Und auch sonst ist vieles hartkodiert und schlecht erweiterbar/wartbar. Also, nur begrenzt ausbaufähig... Und überhaupt ist da die Businesslogik schlecht gekapselt. Vielleicht will ich die Termine vom Chrissi ja im Gegensatz zu dir nicht blau darstellen 😉 Du hast zwar dzau geschrieben, das ist temporär, aber eigentlich das ist das falsche Vorgehen. Erst muss man die Software durchdenken und entsprechend aufbauen und erst dann anfangen irgendwas zu coden, damit man sofort was sehen kann... Um das ganze aber etwas abzumilden: ich hab schon oft wesentlich schilmmeren Code gesehen.
Ich hab grad einen Profiler drüber laufen lassen (die kostenlose Version vom EQATEC Profiler). Das Bild ist eigentlich recht klar. 64% der Zeit wird in OnPaint in der Methode DrawDays verbraten, und davon 100% in der Methode DrawDay. Was mir da sofort auffällt ist, dass du in einer Schleife GDI+ Objekte erstellst und sofort wieder freigibst (das haben auch andere schon angemerkt). Das macht sicehr keinen Sinn. Kann gut sein, dass es 90% der Zeit in Anspruch nimmt.
Ist ja nicht so, dass es bisher keine Scheduler Controls geben würde 😉
kostenlos, mit guter und zeitgemäßer optik, den netten funktionen (terminanordnung bei überlappung, termine in eine ablage packen, rein-/rauszoomen, einstellbare taktung (1, 5, 15min, ...) - nein. habe auch nur zwei bei codeproject gefunden. einen kann man total vergessen, der andere ist basis für meinen gewesen.
Bei dir sind sogar teilweise Farben, Schriften usw. hartkodiert im Quelltext
Und überhaupt ist da die Businesslogik schlecht gekapselt. Vielleicht will ich die Termine vom Chrissi ja im Gegensatz zu dir nicht blau darstellen
äh.. ja! das ist aber auch alles noch nicht relevant für mich gewesen, es geht erstmal nur um die performance der darstellung. da mussten schnelle lösungen her um überhaupt termine auf den schirm zu kriegen.
ich werde natürlich die termineinstellungen vom egtl. control trennen. die software ist durchdacht, nur noch nicht SO implementiert.
ich sagte aber bereits, dass ein großteil provisorisch ist. das control ist weit vom produktivstatus weg. wenn der eintritt, sind keine variablen bestandteile mehr hardcoded - versprochen 😃
dennoch finde ich, dass das projekt ein super ausgangspunkt für jeden ist, der ein scheduler-control bauen will. wer dann meckert, dass styles hardcoded sind, der soll es sich halt einfach selbst umschreiben, die hauptarbeit wurde ja bereits abgenommen 😃
wenn das control final ist, werde ich es hier im forum reinstellen.
64% der Zeit wird in OnPaint in der Methode DrawDays verbraten, und davon 100% in der Methode DrawDay
wow, das hätte ich nicht erwartet. da bin ich aber hoffnungsvoll, dass ich da noch was optimieren kann. die methode zeichnet egtl. nur die fläche im kalender (weiß, blau und zeitstriche). da setzt ich mich gleich mal ran, die gdi objecte private anzulegen und nicht jedes mal neu zu erstellen. danke für den hinweis, dass ich direkt in DrawDay suchen sollte 😃
werde den profiler mal ausprobieren, scheint informativ zu sein!