Neuer Dialog "Deckblätter…" bei Klausuren, Punktetabelle als Drawing mit festen Spaltenbreiten, FLOWDRAWBOX-Attribut fit=content, Stapel-Rendering mit Seitenzahl je Datensatz, Kursbezeichnung und konfigurierbares Kürzel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.8 KiB
C#
83 lines
4.8 KiB
C#
using System.Globalization;
|
|
using LehrerApp.Core.Services;
|
|
using LehrerApp.Templating;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
/// <summary>Zeichnet die Punktetabelle des Klausur-Deckblatts (Aufgabe / Max. Punktzahl /
|
|
/// Punktzahl). Die Spalten haben eine feste Breite: passen nicht alle Aufgaben nebeneinander in die
|
|
/// Box der Vorlage, folgt darunter ein weiterer Tabellenblock, statt alles zu verkleinern. Gedacht
|
|
/// für eine <c>FLOWDRAWBOX … fit=content</c>, damit nachfolgende Elemente mitwandern.</summary>
|
|
public static class ExamPointsTableDrawingBuilder
|
|
{
|
|
public const string PlaceholderName = "Exam.PointsTable";
|
|
|
|
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
|
|
|
|
// Referenzentwurf in Millimetern, abgemessen an der Schulvorlage.
|
|
private const float LabelWidth = 38f, ColumnWidth = 20.5f;
|
|
private const float HeaderHeight = 5.5f, MaximumHeight = 8.5f, PointsHeight = 11.5f;
|
|
private const float BlockGap = 4f, Padding = 1.5f, Stroke = 0.2f, HeavyStroke = 0.45f;
|
|
private const float FontSize = 10f;
|
|
|
|
/// <param name="contentWidth">Breite der deklarierten Box in der Maßeinheit der Vorlage.</param>
|
|
/// <param name="millimeterScale">1 mm in der Maßeinheit der Vorlage (siehe
|
|
/// <see cref="LetterPlaceholderBuilder.FindDeclaredDrawingBox(LoadedTemplate, string)"/>).</param>
|
|
public static DrawingValue Build(IReadOnlyList<ExamCoverTaskColumn> columns, float contentWidth = 170,
|
|
float millimeterScale = 1)
|
|
{
|
|
if (columns.Count == 0) return new DrawingValue([], 0);
|
|
var k = millimeterScale;
|
|
var perBlock = ColumnsPerBlock(contentWidth, millimeterScale);
|
|
// Halbe Strichstärke Abstand zum Rand, sonst schneidet die Box die äußeren Linien an.
|
|
var origin = HeavyStroke / 2 * k;
|
|
var blockHeight = (HeaderHeight + MaximumHeight + PointsHeight) * k;
|
|
var commands = new List<DrawingCommand>();
|
|
var y = origin;
|
|
for (var start = 0; start < columns.Count; start += perBlock)
|
|
{
|
|
if (start > 0) y += BlockGap * k;
|
|
var block = columns.Skip(start).Take(perBlock).ToList();
|
|
var maximumY = y + HeaderHeight * k;
|
|
var pointsY = maximumY + MaximumHeight * k;
|
|
|
|
Cell(commands, origin, y, LabelWidth * k, HeaderHeight * k, "Aufgabe", DrawingTextAlignment.AlignRight, false, k);
|
|
Cell(commands, origin, maximumY, LabelWidth * k, MaximumHeight * k, "Max. Punktzahl", DrawingTextAlignment.AlignRight, false, k);
|
|
Cell(commands, origin, pointsY, LabelWidth * k, PointsHeight * k, "Punktzahl", DrawingTextAlignment.AlignRight, true, k);
|
|
for (var index = 0; index < block.Count; index++)
|
|
{
|
|
var column = block[index];
|
|
var x = origin + (LabelWidth + index * ColumnWidth) * k;
|
|
Cell(commands, x, y, ColumnWidth * k, HeaderHeight * k,
|
|
column.IsBonus ? "" : $"Nr. {column.Number}", DrawingTextAlignment.AlignCenter, false, k);
|
|
Cell(commands, x, maximumY, ColumnWidth * k, MaximumHeight * k,
|
|
(column.IsBonus ? "+" : "") + Format(column.MaxPoints), DrawingTextAlignment.AlignCenter, false, k);
|
|
Cell(commands, x, pointsY, ColumnWidth * k, PointsHeight * k,
|
|
column.AchievedPoints is { } achieved ? Format(achieved) : "", DrawingTextAlignment.AlignCenter, false, k);
|
|
}
|
|
commands.Add(new DrawLine(origin, pointsY, origin + (LabelWidth + block.Count * ColumnWidth) * k, pointsY,
|
|
StrokeWidth: HeavyStroke * k));
|
|
y += blockHeight;
|
|
}
|
|
return new DrawingValue(commands, y + origin);
|
|
}
|
|
|
|
/// <summary>Wie viele Aufgaben bei fester Spaltenbreite nebeneinander in die Box passen.</summary>
|
|
public static int ColumnsPerBlock(float contentWidth, float millimeterScale = 1) =>
|
|
Math.Max(1, (int)Math.Floor((contentWidth / millimeterScale - HeavyStroke - LabelWidth + 0.01f) / ColumnWidth));
|
|
|
|
private static void Cell(List<DrawingCommand> commands, float x, float y, float width, float height,
|
|
string text, DrawingTextAlignment alignment, bool bold, float k)
|
|
{
|
|
commands.Add(new DrawRectangle(x, y, width, height, StrokeWidth: Stroke * k));
|
|
if (text.Length == 0) return;
|
|
// DrawStringEx setzt die Grundlinie auf Y + Schriftgröße; so sitzt die Zeile optisch mittig.
|
|
var fontHeight = FontSize * UnitConverter.Points(1, "pt") / UnitConverter.Points(1, "mm") * k;
|
|
var textY = y + height / 2 - fontHeight * 0.64f;
|
|
commands.Add(new DrawStringEx(x + Padding * k, textY, y + height - textY, width - 2 * Padding * k, text,
|
|
alignment, FontSize, Bold: bold));
|
|
}
|
|
|
|
private static string Format(double value) => value.ToString("0.##", German);
|
|
}
|