protected override void OnRender(DrawingContext drawingContext) { try { using (IsGraphDrawnScope isGraphDrawnScope = new IsGraphDrawnScope()) { if (RenderSize.Width < 10.0 || RenderSize.Height < 10.0) { return; } if (PBMCommand.Instance == null) { return; } DTE2 dte = (DTE2)PBMCommand.Instance.ServiceProvider.GetService(typeof(DTE)); if (dte == null) { return; } if (DataModel.Instance.AllProjectsCount == 0) { return; } int linesCount = DataModel.CurrentBuilds.Count + DataModel.FinishedBuilds.Count + 1 + 1 + 1 + 1; // 1 for header, 1 for status, 1 for CPU, 1 for HDD double totalHeight = rowHeight * linesCount; Height = totalHeight + penThickness; long tickStep = 100000000; long maxTick = tickStep; long nowTick = DateTime.Now.Ticks; long t = nowTick - DataModel.StartTime.Ticks; if (DataModel.CurrentBuilds.Count > 0) { if (t > maxTick) { maxTick = t; } } int ii; bool atLeastOneError = false; uint maxBuildOrderNbr = 1; double projectNameMaxLen = 10; for (ii = 0; ii < DataModel.FinishedBuilds.Count; ii++) { FormattedText iname = new FormattedText(DataModel.FinishedBuilds[ii].ProjectName, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush); double ll = iname.Width; t = DataModel.FinishedBuilds[ii].end; atLeastOneError = atLeastOneError || !DataModel.FinishedBuilds[ii].success; if (t > maxTick) { maxTick = t; } if (ll > projectNameMaxLen) { projectNameMaxLen = ll; } if (DataModel.FinishedBuilds[ii].ProjectBuildOrderNumber > maxBuildOrderNbr) { maxBuildOrderNbr = DataModel.FinishedBuilds[ii].ProjectBuildOrderNumber; } } foreach (KeyValuePair <string, Tuple <uint, long> > item in DataModel.CurrentBuilds) { FormattedText iname = new FormattedText(DataModel.GetHumanReadableProjectName(item.Key), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush); double ll = iname.Width; if (ll > projectNameMaxLen) { projectNameMaxLen = ll; } if (item.Value.Item1 > maxBuildOrderNbr) { maxBuildOrderNbr = item.Value.Item1; } } if (isBuilding) { maxTick = (maxTick / tickStep + 1) * tickStep; } Spacings spacings = new Spacings(); { //setting spacings string pattern = "> "; int len = maxBuildOrderNbr.ToString().Length + pattern.Length; pattern = pattern.PadLeft(len, '8'); // let's assume that 8 is the widest char FormattedText bn = new FormattedText(pattern, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush); spacings.lProjName = Spacings.lOrder + bn.Width + 3; // let's add 3 pix just in case 8 is not the widest spacings.lGanttC = spacings.lProjName + projectNameMaxLen + penThickness + 3; // let's add 3 pix just in case } int rowNbr = 0; //first row has number 0 { // Draw header DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing string headerText = DataModel.GetSolutionNameWithMachineInfo(" | ", false /*WithBuildStartedStr*/); // Draw text FormattedText itext = new FormattedText(headerText, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush); { // Cut text when it is too long for window. Probably correct solution is to add horizontal scrollbar to window. // Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears. itext.MaxTextWidth = RenderSize.Width - Spacings.lOrder - Spacings.rGanttC; itext.MaxTextHeight = rowHeight; } drawingContext.DrawText(itext, new Point(Spacings.lOrder, rowNbr * rowHeight)); rowNbr++; } foreach (BuildInfo item in DataModel.FinishedBuilds) { DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawOrderAndProjectNameText(drawingContext, rowNbr, item.ProjectBuildOrderNumber, item.ProjectName, spacings, (item.success ? greenSolidBrush : redSolidBrush)); DrawBar(drawingContext, rowNbr, item.begin, item.end, maxTick, spacings, (item.success ? greenGradientBrush : redGradientBrush), DataModel.CriticalPath.Contains(item)); rowNbr++; } foreach (KeyValuePair <string, Tuple <uint, long> > item in DataModel.CurrentBuilds) { DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawOrderAndProjectNameText(drawingContext, rowNbr, item.Value.Item1, DataModel.GetHumanReadableProjectName(item.Key), spacings, blueSolidBrush); DrawBar(drawingContext, rowNbr, item.Value.Item2, (nowTick - DataModel.StartTime.Ticks), maxTick, spacings, blueGradientBrush, false /*markAsCriticalPath*/); rowNbr++; } DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawGraph("CPU usage", drawingContext, DataModel.CpuUsage, cpuPen, cpuSoftPen, rowNbr, RenderSize, rowHeight, spacings, maxTick, nowTick, fontFace); rowNbr++; DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawGraph("HDD usage", drawingContext, DataModel.HddUsage, hddPen, hddSoftPen, rowNbr, RenderSize, rowHeight, spacings, maxTick, nowTick, fontFace); rowNbr++; if (DataModel.CurrentBuilds.Count > 0 || DataModel.FinishedBuilds.Count > 0) { string status = (isBuilding ? "Building..." : "Done"); if (DataModel.MaxParallelBuilds > 0) { status += " (" + DataModel.PercentageProcessorUse().ToString() + "% of " + DataModel.MaxParallelBuilds.ToString() + " CPUs)"; } DrawText(drawingContext, status, rowNbr, Spacings.lOrder, blackBrush); } DrawVerticalSeparator(drawingContext, spacings.lGanttC - penThickness, rowNbr, false /*wholeSize*/); drawingContext.DrawLine(new Pen(atLeastOneError ? redSolidBrush : greenSolidBrush, 1), new Point(0, rowNbr * rowHeight), new Point(RenderSize.Width, rowNbr * rowHeight)); DateTime dt = new DateTime(maxTick); string s = Utils.SecondsToString(dt.Ticks); FormattedText maxTime = new FormattedText(s, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush); double m = maxTime.Width; drawingContext.DrawText(maxTime, new Point(RenderSize.Width - m - Spacings.rGanttC, rowNbr * rowHeight)); { // Draw frame around - it looks better in window and definitelly in saved .png. Draw frame as last to be clearly visible DrawSeparator(drawingContext, -1); // Top DrawSeparator(drawingContext, rowNbr); // Bottom DrawVerticalSeparator(drawingContext, 0, rowNbr, true /*wholeSize*/); // Left DrawVerticalSeparator(drawingContext, RenderSize.Width - penThickness, rowNbr, true /*wholeSize*/); // Right } isGraphDrawnScope.IsDrawn = true; } //End of IsGraphDrawnScope } catch (Exception) { // Keep this try{} catch{}! // Actually code in try{} bail from time to time on windows resize. // I guess because there is division by (x - y), while x equals y, but it might be not the only case. Debug.Assert(false, "Gantt chart not refreshed! Exception thrown while drawing Gantt chart."); } }
private void DrawBar(DrawingContext drawingContext, int rowNbr, long startTime, long endTime, long maxTick, Spacings spacings, Brush color, bool markAsCriticalPath) { double pixelRange = RenderSize.Width - spacings.lGanttC - Spacings.rGanttC; if (pixelRange < minGanttWidth) { return; } Rect r = new Rect(); // Why (int) and (long) is needed here? Let's try to simplify //r.X = spacings.lGanttC + (int)(startTime * (long)(pixelRange) / maxTick); //r.Width = spacings.lGanttC + (int)(endTime * (long)(pixelRange) / maxTick) - r.X; r.X = spacings.lGanttC + (startTime * pixelRange / maxTick); r.Width = spacings.lGanttC + (endTime * pixelRange / maxTick) - r.X; if (r.Width == 0) { r.Width = 1; } r.Y = rowNbr * rowHeight + 1; r.Height = rowHeight - 1; drawingContext.DrawRectangle(color, null, r); //Draw Gantt graph if (markAsCriticalPath) { Rect cPR = new Rect(r.Location, r.Size); double cPRLHeight = r.Height / 8; cPR.Height = cPRLHeight; drawingContext.DrawRectangle(criticalPathGradientBrush, null, cPR); //Draw top yellow line in Gantt graph cPR.Y += (rowHeight - cPRLHeight - penThickness); drawingContext.DrawRectangle(criticalPathGradientBrush, null, cPR); //Draw bottom yellow line in Gantt graph } string time = Utils.SecondsToString(endTime - startTime); FormattedText itime = new FormattedText(time, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, whiteBrush); double timeLen = itime.Width; if (r.Width > timeLen) { drawingContext.DrawText(itime, new Point(r.Right - timeLen, rowNbr * rowHeight)); //Write elapsed time } }
void DrawGraph(string title, DrawingContext drawingContext, ReadOnlyCollection <Tuple <long, float> > data, Pen pen, Pen softPen, int rowNbr, Size RenderSize, double rowHeight, Spacings spacings, long maxTick, long nowTick, Typeface fontFace) { if (data.Count < 1) { return; } double pixelRange = RenderSize.Width - spacings.lGanttC - Spacings.rGanttC; if (pixelRange < minGanttWidth) { DrawText(drawingContext, title, rowNbr, Spacings.lOrder, blackBrush); return; } drawingContext.PushClip(new RectangleGeometry(new Rect(0, rowNbr * rowHeight, RenderSize.Width, rowHeight))); double sumValues = 0; long sumTicks = 0; long previousTick = DataModel.StartTime.Ticks; float previousValue = 0.0f; int step = 1; if (data.Count > 10) // Do rarefy only when more than 10 samples { step = (int)(data.Count / (pixelRange / 10)); // Draw no frequent than 10 pixels if (step < 1) { step = 1; } } var allPoints = new List <Point>(); for (int nbr = 0; nbr < data.Count; nbr += step) { long spanL = previousTick - DataModel.StartTime.Ticks; long spanR = data[nbr].Item1 - DataModel.StartTime.Ticks; if (isBuilding && nbr >= data.Count - step) { spanR = nowTick - DataModel.StartTime.Ticks; } // Why (int) and (long) is needed here? Let's try to simplify //double shiftL = (int)(spanL * (long)pixelRange / maxTick); //double shiftR = (int)(spanR * (long)pixelRange / maxTick); double shiftL = spanL * pixelRange / maxTick; double shiftR = spanR * pixelRange / maxTick; Point p1 = new Point(spacings.lGanttC + shiftL, (rowNbr + 1) * rowHeight - previousValue * (rowHeight - 2) / 100 - 1); Point p2 = new Point(spacings.lGanttC + shiftR, (rowNbr + 1) * rowHeight - data[nbr].Item2 * (rowHeight - 2) / 100 - 1); StreamGeometry streamGeometry = new StreamGeometry(); using (StreamGeometryContext geometryContext = streamGeometry.Open()) { geometryContext.BeginFigure(p1, true, true); Point p3 = new Point(p2.X, (rowNbr + 1) * rowHeight); Point p4 = new Point(p1.X, (rowNbr + 1) * rowHeight); PointCollection points = new PointCollection { p2, p3, p4 }; geometryContext.PolyLineTo(points, true, true); } drawingContext.DrawGeometry(softPen.Brush, softPen, streamGeometry); if (nbr == 0) { allPoints.Add(p1); } allPoints.Add(p2); if (nbr > 0) { sumValues += (previousValue + data[nbr].Item2) * (data[nbr].Item1 - previousTick); sumTicks += data[nbr].Item1 - previousTick; } previousTick = data[nbr].Item1; previousValue = data[nbr].Item2; } for (int nbr = 1; nbr < allPoints.Count; ++nbr) { drawingContext.DrawLine(pen, allPoints[nbr - 1], allPoints[nbr]); } if (sumTicks > 0) { long average = (long)(sumValues / 2 / sumTicks); FormattedText avg = new FormattedText(" (Avg. " + average.ToString() + "%)", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush); title += avg.Text; } DrawText(drawingContext, title, rowNbr, Spacings.lOrder, blackBrush); drawingContext.Pop(); }
private void DrawOrderAndProjectNameText(DrawingContext drawingContext, int rowNbr, uint projectBuildOrderNumber, string projectName, Spacings margins, Brush textColor) { // project build Order DrawText(drawingContext, projectBuildOrderNumber.ToString() + ">", rowNbr, Spacings.lOrder, textColor); // project Name DrawText(drawingContext, projectName, rowNbr, margins.lProjName, textColor); }
protected override void OnRender(DrawingContext drawingContext) { try { using (IsGraphDrawnScope isGraphDrawnScope = new IsGraphDrawnScope()) { if (RenderSize.Width < 10.0 || RenderSize.Height < 10.0) { return; } if (IsEmptyBuilds()) { // Case when no single build was started yet - display some info to ensure user that everything is OK FormattedText captionFT = new FormattedText(emptyGanttMsg, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip); drawingContext.DrawText(captionFT, new Point(50.0, rowNbrNoProjectsFiller / 2 * rowHeight)); return; } int linesCount = DataModel.CurrentBuilds.Count + DataModel.FinishedBuilds.Count + 1 + 1 + 1 + 1; // 1 for header, 1 for status, 1 for CPU, 1 for HDD double totalHeight = rowHeight * linesCount; Height = totalHeight + penThickness; long tickStep = 100000000; long maxTick = tickStep; long nowTick = ((nowTickForTest > 0) ? nowTickForTest : DateTime.Now.Ticks); long t = nowTick - DataModel.StartTime.Ticks; if (DataModel.CurrentBuilds.Count > 0) { if (t > maxTick) { maxTick = t; } } int ii; bool atLeastOneError = false; uint maxBuildOrderNbr = 1; double projectNameMaxLen = 10; for (ii = 0; ii < DataModel.FinishedBuilds.Count; ii++) { FormattedText iname = new FormattedText(DataModel.FinishedBuilds[ii].ProjectName, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip); double ll = iname.Width; t = DataModel.FinishedBuilds[ii].end; atLeastOneError = atLeastOneError || !DataModel.FinishedBuilds[ii].success; if (t > maxTick) { maxTick = t; } if (ll > projectNameMaxLen) { projectNameMaxLen = ll; } if (DataModel.FinishedBuilds[ii].ProjectBuildOrderNumber > maxBuildOrderNbr) { maxBuildOrderNbr = DataModel.FinishedBuilds[ii].ProjectBuildOrderNumber; } } foreach (KeyValuePair <string, Tuple <uint, long> > item in DataModel.CurrentBuilds) { FormattedText iname = new FormattedText(DataModel.GetHumanReadableProjectName(item.Key), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip); double ll = iname.Width; if (ll > projectNameMaxLen) { projectNameMaxLen = ll; } if (item.Value.Item1 > maxBuildOrderNbr) { maxBuildOrderNbr = item.Value.Item1; } } if (DataModel.IsBuilding) { maxTick = (maxTick / tickStep + 1) * tickStep; } Spacings spacings = new Spacings(); { //setting spacings string pattern = "> "; int len = maxBuildOrderNbr.ToString().Length + pattern.Length; pattern = pattern.PadLeft(len, '8'); // let's assume that 8 is the widest char FormattedText bn = new FormattedText(pattern, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip); spacings.lProjName = Spacings.lOrder + bn.Width + 3; // let's add 3 pix just in case 8 is not the widest spacings.lGanttC = spacings.lProjName + projectNameMaxLen + penThickness + 3; // let's add 3 pix just in case } // check if usage text is longer than the longest project name, and yes, values greater than 1000% are possible double usageTextLen = new FormattedText("HDD Usage (Avg. 1000%)", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip).Width; spacings.lGanttC = Math.Max(spacings.lGanttC, Spacings.lOrder + usageTextLen + penThickness); int rowNbr = 0; //first row has number 0 { // Draw header DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawMachineInfo(drawingContext, rowNbr); rowNbr++; } foreach (BuildInfo item in DataModel.FinishedBuilds) { DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawOrderAndProjectNameText(drawingContext, rowNbr, item.ProjectBuildOrderNumber, item.ProjectName, spacings, (item.success ? greenSolidBrush : redSolidBrush)); DrawBar(drawingContext, rowNbr, item.begin, item.end, maxTick, spacings, (item.success ? greenGradientBrush : redGradientBrush), DataModel.CriticalPath.Contains(item)); rowNbr++; } foreach (KeyValuePair <string, Tuple <uint, long> > item in DataModel.CurrentBuilds) { DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawOrderAndProjectNameText(drawingContext, rowNbr, item.Value.Item1, DataModel.GetHumanReadableProjectName(item.Key), spacings, blueSolidBrush); DrawBar(drawingContext, rowNbr, item.Value.Item2, (nowTick - DataModel.StartTime.Ticks), maxTick, spacings, blueGradientBrush, false /*markAsCriticalPath*/); rowNbr++; } DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawGraph("CPU usage", drawingContext, DataModel.CpuUsage, cpuPen, cpuSoftPen, rowNbr, RenderSize, rowHeight, spacings, maxTick, nowTick, fontFace); rowNbr++; DrawSeparator(drawingContext, rowNbr); // draw backgroud things first due to anty-aliasing DrawGraph("HDD usage", drawingContext, DataModel.HddUsage, hddPen, hddSoftPen, rowNbr, RenderSize, rowHeight, spacings, maxTick, nowTick, fontFace); rowNbr++; if (DataModel.CurrentBuilds.Count > 0 || DataModel.FinishedBuilds.Count > 0) { string status = (DataModel.IsBuilding ? "Building..." : "Done."); if (DataModel.MaxParallelBuilds > 0) { status += " Max. no. of parallel projects: " + DataModel.MaxParallelBuilds.ToString() + " (Avg. " + Math.Round(DataModel.MaxParallelBuilds / 100.0 * DataModel.PercentageProcessorUse(), 1).ToString(format: "0.0") + ")"; } DrawText(drawingContext, status, rowNbr, Spacings.lOrder, blackBrush); } DrawVerticalSeparator(drawingContext, spacings.lGanttC - penThickness, rowNbr, false /*wholeSize*/); drawingContext.DrawLine(new Pen(atLeastOneError ? redSolidBrush : greenSolidBrush, 1), new Point(0, rowNbr * rowHeight), new Point(RenderSize.Width, rowNbr * rowHeight)); DateTime dt = new DateTime(maxTick); string s = Utils.SecondsToString(dt.Ticks); FormattedText maxTime = new FormattedText(s, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, fontFace, FontSize, blackBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip); double m = maxTime.Width; drawingContext.DrawText(maxTime, new Point(RenderSize.Width - m - Spacings.rGanttC, rowNbr * rowHeight)); { // Draw frame around - it looks better in window and definitelly in saved .png. Draw frame as last to be clearly visible DrawSeparator(drawingContext, -1); // Top DrawSeparator(drawingContext, rowNbr); // Bottom DrawVerticalSeparator(drawingContext, 0, rowNbr, true /*wholeSize*/); // Left DrawVerticalSeparator(drawingContext, RenderSize.Width - penThickness, rowNbr, true /*wholeSize*/); // Right } isGraphDrawnScope.IsDrawn = true; } //End of IsGraphDrawnScope } catch (Exception) { Debug.Assert(false, "Gantt chart not refreshed! Exception thrown while drawing Gantt chart."); } }