質問

I have a bunch of icons that should be used at many different places in different sizes across a Windows Phone 8 app. Using plain graphics I would have to create each icon in each size for each supported resolution. This can become a lot of files...

Thus I would like to use vector graphics instead. Since I have all icons as PDFs / Illustrator .ai this not a problem. I used Blend to import these files and it created XAML automatically:

<Canvas x:Name="TestIcon" Height="27.985" Canvas.Left="5.506" Canvas.Top="2.007" Width="20.988">
    <Path Data="F1M26.992,... Fill="#FF376EB5" Height="27.977" Canvas.Left="0" Stretch="None" Canvas.Top="0" Width="27.993">
        <Path.Clip>
            <RectangleGeometry Rect="0,-0.001,27.992,27.977"/>
         </Path.Clip>
    </Path>
</Canvas>

The Problem: How can I use this canvas all over the app?

I added the Canvas to the Application.Resources and tried to reference it:

<!-- Version 1 -->
<Button Content="{StaticResource TestIcon} .../>

<!-- Version 2 -->
<Button ...>
    <ContentPresenter Content="{StaticResource TestIcon}" .../>
</Button>

<!-- Version 3 -->
<Button ...>
    <Canvas ..>
        <Path Data="F1M26.992,... ...>
            <Path.Clip>
                ...
             </Path.Clip>
        </Path>
    </Canvas>
</Button>

When using Version 1 or 2 everything works fine in the Designer. As soon as I run the app it crashen with an System.Windows.Markup.XamlParseException:

Failed to assign to property 'System.Windows.Controls.ContentPresenter.Content'.

Version 3 works fine both in Designer and at runtime. But of course this solution does not use any reference to the VectorGraphic but includes the Canvas directly.

Any idea how to solve this?

役に立ちましたか?

解決

You should probably just create ControlTemplate for each of your shapes in Resources and then reuse this template anywhere in your app:

<ControlTemplate x:Key="MyIcon">
    <Canvas x:Name="TestIcon" Height="27.985" Canvas.Left="5.506" Canvas.Top="2.007" Width="20.988">
        <Path Data="F1M26.992,... Fill="#FF376EB5" Height="27.977" Canvas.Left="0" Stretch="None" Canvas.Top="0" Width="27.993">
            <Path.Clip>
                <RectangleGeometry Rect="0,-0.001,27.992,27.977"/>
             </Path.Clip>
        </Path>
    </Canvas>
</ControlTemplate>

on another place, where you want to show  it:
<ContentControl Template="{StaticResource MyIcon}"/>

他のヒント

Check this out: Reusing Vector Graphics in Windows 8 Store Apps

There's total 3 ways to reuse the XAML path

  1. Using path itself

    <Border Height="200"
        Width="200"
        BorderBrush="Red"
        BorderThickness="1"
        Background="Yellow"
        Padding="20"
        Grid.Row="1"
        Grid.Column="1">
    <Viewbox Stretch="Uniform">
        <!-- Works at designtime but crashes at runtime -->
        <!--<ContentControl Content="{StaticResource PrancingHorse}" />-->    
        <!-- That would work if if were possible to define the PathGeomatry as a resource. -->
        <!--<Path Data="{StaticResource PrancingGeometry}" />-->
        <Path Data="M22.923445,61.752014L22.733789,61.7598....."
                Fill="White" />
    </Viewbox>
    

  2. Using path data as string resource

    <Page.Resources>
        <x:String x:Key="PrancingString">M22.923445,61.752014L22.733789,61.759857 .....</x:String>
    </Page.Resources>
    
    <Border Height="200"
        Width="200"
        BorderBrush="Red"
        BorderThickness="1"
        Padding="20"
        Grid.Row="3"
        Grid.Column="2">
    <Viewbox Stretch="Uniform">
        <Path Data="{StaticResource PrancingString}"
              Fill="White" />
    </Viewbox>
    

  3. Using icon control

IconControl.cs

public sealed class IconControl : Control
{
    public static readonly DependencyProperty DataGeometryProperty =
        DependencyProperty.Register("DataGeometry", typeof(PathGeometry), typeof(IconControl), new PropertyMetadata(null));

    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(String), typeof(IconControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDataChanged)));

    public IconControl()
    {
        this.DefaultStyleKey = typeof(IconControl);
    }

    // Write-only to be used in a binding.
    public String Data
    {
        private get { return (String)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    // Read-only to be used in the control's template.
    public PathGeometry DataGeometry
    {
        get { return (PathGeometry)GetValue(DataGeometryProperty); }
        private set { SetValue(DataGeometryProperty, value); }
    }

    private static void OnDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        IconControl ic = d as IconControl;

        // In WPF: ic.DataGeometry = PathGeometry.CreateFromGeometry(PathGeometry.Parse(e.NewValue.ToString()));
        ic.DataGeometry = new PathGeometryParser().Parse(e.NewValue.ToString());
    }
}

PathGeometryParser.cs

public class PathGeometryParser
{
    #region Constants
    const bool AllowSign = true;
    const bool AllowComma = true;
    const bool IsFilled = true;
    const bool IsClosed = true; 
    #endregion

    #region Fields
    IFormatProvider _formatProvider;

    PathFigure _figure = null;     // Figure object, which will accept parsed segments
    string _pathString;        // Input string to be parsed
    int _pathLength;
    int _curIndex;          // Location to read next character from
    bool _figureStarted;     // StartFigure is effective 

    Point _lastStart;         // Last figure starting point
    Point _lastPoint;         // Last point 
    Point _secondLastPoint;   // The point before last point

    char _token = ' ';             // Non whitespace character returned by ReadToken 
    #endregion

    static internal char GetNumericListSeparator(IFormatProvider provider)
    {
        char numericSeparator = ',';

        // Get the NumberFormatInfo out of the provider, if possible
        // If the IFormatProvider doesn't not contain a NumberFormatInfo, then 
        // this method returns the current culture's NumberFormatInfo. 
        NumberFormatInfo numberFormat = NumberFormatInfo.GetInstance(provider);

        // Is the decimal separator is the same as the list separator?
        // If so, we use the ";". 
        if ((numberFormat.NumberDecimalSeparator.Length > 0) && (numericSeparator == numberFormat.NumberDecimalSeparator[0]))
        {
            numericSeparator = ';';
        }

        return numericSeparator;
    }

    /// <summary>
    /// Turns a string into a PathGeometry.
    /// </summary>
    /// <remarks>
    /// Code could use some refactoring, to expose the logic as an extension method of PathGeometry.
    /// </remarks>
    public PathGeometry Parse(string path)
    {
        PathGeometry _pathGeometry = null;

        _formatProvider = CultureInfo.InvariantCulture;
        _pathString = path;
        _pathLength = path.Length;
        _curIndex = 0;

        _secondLastPoint = new Point(0, 0);
        _lastPoint = new Point(0, 0);
        _lastStart = new Point(0, 0);

        _figureStarted = false;

        bool first = true;

        char last_cmd = ' ';

        while (ReadToken()) // Empty path is allowed in XAML
        {
            char cmd = _token;

            if (first)
            {
                if ((cmd != 'M') && (cmd != 'm') && (cmd != 'f') && (cmd != 'F'))  // Path starts with M|m 
                {
                    ThrowBadToken();
                }

                first = false;
            }

            switch (cmd)
            {
                case 'f':
                case 'F':
                    _pathGeometry = new PathGeometry();
                    double _num = ReadNumber(!AllowComma);
                    _pathGeometry.FillRule = _num == 0 ? FillRule.EvenOdd : FillRule.Nonzero;
                    break;

                case 'm':
                case 'M':
                    // XAML allows multiple points after M/m
                    _lastPoint = ReadPoint(cmd, !AllowComma);

                    _figure = new PathFigure();
                    _figure.StartPoint = _lastPoint;
                    _figure.IsFilled = IsFilled;
                    _figure.IsClosed = !IsClosed;
                    //context.BeginFigure(_lastPoint, IsFilled, !IsClosed);
                    _figureStarted = true;
                    _lastStart = _lastPoint;
                    last_cmd = 'M';

                    while (IsNumber(AllowComma))
                    {
                        _lastPoint = ReadPoint(cmd, !AllowComma);

                        LineSegment _lineSegment = new LineSegment();
                        _lineSegment.Point = _lastPoint;
                        _figure.Segments.Add(_lineSegment);
                        //context.LineTo(_lastPoint, IsStroked, !IsSmoothJoin);
                        last_cmd = 'L';
                    }
                    break;

                case 'l':
                case 'L':
                case 'h':
                case 'H':
                case 'v':
                case 'V':
                    EnsureFigure();

                    do
                    {
                        switch (cmd)
                        {
                            case 'l': _lastPoint = ReadPoint(cmd, !AllowComma); break;
                            case 'L': _lastPoint = ReadPoint(cmd, !AllowComma); break;
                            case 'h': _lastPoint.X += ReadNumber(!AllowComma); break;
                            case 'H': _lastPoint.X = ReadNumber(!AllowComma); break;
                            case 'v': _lastPoint.Y += ReadNumber(!AllowComma); break;
                            case 'V': _lastPoint.Y = ReadNumber(!AllowComma); break;
                        }

                        LineSegment _lineSegment = new LineSegment();
                        _lineSegment.Point = _lastPoint;
                        _figure.Segments.Add(_lineSegment);
                        //context.LineTo(_lastPoint, IsStroked, !IsSmoothJoin);
                    }
                    while (IsNumber(AllowComma));

                    last_cmd = 'L';
                    break;

                case 'c':
                case 'C': // cubic Bezier 
                case 's':
                case 'S': // smooth cublic Bezier
                    EnsureFigure();

                    do
                    {
                        Point p;

                        if ((cmd == 's') || (cmd == 'S'))
                        {
                            if (last_cmd == 'C')
                            {
                                p = Reflect();
                            }
                            else
                            {
                                p = _lastPoint;
                            }

                            _secondLastPoint = ReadPoint(cmd, !AllowComma);
                        }
                        else
                        {
                            p = ReadPoint(cmd, !AllowComma);

                            _secondLastPoint = ReadPoint(cmd, AllowComma);
                        }

                        _lastPoint = ReadPoint(cmd, AllowComma);

                        BezierSegment _bizierSegment = new BezierSegment();
                        _bizierSegment.Point1 = p;
                        _bizierSegment.Point2 = _secondLastPoint;
                        _bizierSegment.Point3 = _lastPoint;
                        _figure.Segments.Add(_bizierSegment);
                        //context.BezierTo(p, _secondLastPoint, _lastPoint, IsStroked, !IsSmoothJoin);

                        last_cmd = 'C';
                    }
                    while (IsNumber(AllowComma));

                    break;

                case 'q':
                case 'Q': // quadratic Bezier 
                case 't':
                case 'T': // smooth quadratic Bezier
                    EnsureFigure();

                    do
                    {
                        if ((cmd == 't') || (cmd == 'T'))
                        {
                            if (last_cmd == 'Q')
                            {
                                _secondLastPoint = Reflect();
                            }
                            else
                            {
                                _secondLastPoint = _lastPoint;
                            }

                            _lastPoint = ReadPoint(cmd, !AllowComma);
                        }
                        else
                        {
                            _secondLastPoint = ReadPoint(cmd, !AllowComma);
                            _lastPoint = ReadPoint(cmd, AllowComma);
                        }

                        QuadraticBezierSegment _quadraticBezierSegment = new QuadraticBezierSegment();
                        _quadraticBezierSegment.Point1 = _secondLastPoint;
                        _quadraticBezierSegment.Point2 = _lastPoint;
                        _figure.Segments.Add(_quadraticBezierSegment);
                        //context.QuadraticBezierTo(_secondLastPoint, _lastPoint, IsStroked, !IsSmoothJoin);

                        last_cmd = 'Q';
                    }
                    while (IsNumber(AllowComma));

                    break;

                case 'a':
                case 'A':
                    EnsureFigure();

                    do
                    {
                        // A 3,4 5, 0, 0, 6,7
                        double w = ReadNumber(!AllowComma);
                        double h = ReadNumber(AllowComma);
                        double rotation = ReadNumber(AllowComma);
                        bool large = ReadBool();
                        bool sweep = ReadBool();

                        _lastPoint = ReadPoint(cmd, AllowComma);

                        ArcSegment _arcSegment = new ArcSegment();
                        _arcSegment.Point = _lastPoint;
                        _arcSegment.Size = new Size(w, h);
                        _arcSegment.RotationAngle = rotation;
                        _arcSegment.IsLargeArc = large;
                        _arcSegment.SweepDirection = sweep ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
                        _figure.Segments.Add(_arcSegment);
                        //context.ArcTo(
                        //    _lastPoint,
                        //    new Size(w, h),
                        //    rotation,
                        //    large,
                        //    sweep ? SweepDirection.Clockwise : SweepDirection.Counterclockwise,
                        //    IsStroked,
                        //    !IsSmoothJoin
                        //    );
                    }
                    while (IsNumber(AllowComma));

                    last_cmd = 'A';
                    break;

                case 'z':
                case 'Z':
                    EnsureFigure();
                    _figure.IsClosed = IsClosed;
                    //context.SetClosedState(IsClosed);

                    _figureStarted = false;
                    last_cmd = 'Z';

                    _lastPoint = _lastStart; // Set reference point to be first point of current figure
                    break;

                default:
                    ThrowBadToken();
                    break;
            }

            if (null != _figure)
            {
                if (_figure.IsClosed)
                {
                    if (null == _pathGeometry)
                        _pathGeometry = new PathGeometry();

                    _pathGeometry.Figures.Add(_figure);

                    _figure = null;
                    first = true;
                }
            }


        }

        if (null != _figure)
        {
            if (null == _pathGeometry)
                _pathGeometry = new PathGeometry();

            if (!_pathGeometry.Figures.Contains(_figure))
                _pathGeometry.Figures.Add(_figure);

        }
        return _pathGeometry;
    }

    // Not used! Just here for the completeness.
    public string ParseBack(PathGeometry geometry)
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        IFormatProvider provider = new System.Globalization.CultureInfo("en-us");
        string format = null;

        sb.Append("F" + (geometry.FillRule == FillRule.EvenOdd ? "0" : "1") + " ");

        foreach (PathFigure figure in geometry.Figures)
        {
            sb.Append("M " + ((IFormattable)figure.StartPoint).ToString(format, provider) + " ");

            foreach (PathSegment segment in figure.Segments)
            {
                char separator = GetNumericListSeparator(provider);

                if (segment.GetType() == typeof(LineSegment))
                {
                    LineSegment _lineSegment = segment as LineSegment;

                    sb.Append("L " + ((IFormattable)_lineSegment.Point).ToString(format, provider) + " ");
                }
                else if (segment.GetType() == typeof(BezierSegment))
                {
                    BezierSegment _bezierSegment = segment as BezierSegment;

                    sb.Append(String.Format(provider,
                            "C{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "} ",
                            separator,
                            _bezierSegment.Point1,
                            _bezierSegment.Point2,
                            _bezierSegment.Point3
                            ));
                }
                else if (segment.GetType() == typeof(QuadraticBezierSegment))
                {
                    QuadraticBezierSegment _quadraticBezierSegment = segment as QuadraticBezierSegment;

                    sb.Append(String.Format(provider,
                            "Q{1:" + format + "}{0}{2:" + format + "} ",
                            separator,
                            _quadraticBezierSegment.Point1,
                            _quadraticBezierSegment.Point2));
                }
                else if (segment.GetType() == typeof(ArcSegment))
                {
                    ArcSegment _arcSegment = segment as ArcSegment;

                    sb.Append(String.Format(provider,
                            "A{1:" + format + "}{0}{2:" + format + "}{0}{3}{0}{4}{0}{5:" + format + "} ",
                            separator,
                            _arcSegment.Size,
                            _arcSegment.RotationAngle,
                            _arcSegment.IsLargeArc ? "1" : "0",
                            _arcSegment.SweepDirection == SweepDirection.Clockwise ? "1" : "0",
                            _arcSegment.Point));
                }
            }

            if (figure.IsClosed)
                sb.Append("Z");
        }

        return sb.ToString();
    }

    #region Privates
    private void SkipDigits(bool signAllowed)
    {
        // Allow for a sign 
        if (signAllowed && More() && ((_pathString[_curIndex] == '-') || _pathString[_curIndex] == '+'))
        {
            _curIndex++;
        }

        while (More() && (_pathString[_curIndex] >= '0') && (_pathString[_curIndex] <= '9'))
        {
            _curIndex++;
        }
    }

    private bool ReadBool()
    {
        SkipWhiteSpace(AllowComma);

        if (More())
        {
            _token = _pathString[_curIndex++];

            if (_token == '0')
            {
                return false;
            }
            else if (_token == '1')
            {
                return true;
            }
        }

        ThrowBadToken();

        return false;
    }

    private Point Reflect()
    {
        return new Point(2 * _lastPoint.X - _secondLastPoint.X,
                            2 * _lastPoint.Y - _secondLastPoint.Y);
    }

    private void EnsureFigure()
    {
        if (!_figureStarted)
        {
            _figure = new PathFigure();
            _figure.StartPoint = _lastStart;

            //_context.BeginFigure(_lastStart, IsFilled, !IsClosed);
            _figureStarted = true;
        }
    }

    private double ReadNumber(bool allowComma)
    {
        if (!IsNumber(allowComma))
        {
            ThrowBadToken();
        }

        bool simple = true;
        int start = _curIndex;

        //
        // Allow for a sign
        //
        // There are numbers that cannot be preceded with a sign, for instance, -NaN, but it's 
        // fine to ignore that at this point, since the CLR parser will catch this later.
        // 
        if (More() && ((_pathString[_curIndex] == '-') || _pathString[_curIndex] == '+'))
        {
            _curIndex++;
        }

        // Check for Infinity (or -Infinity).
        if (More() && (_pathString[_curIndex] == 'I'))
        {
            // 
            // Don't bother reading the characters, as the CLR parser will 
            // do this for us later.
            // 
            _curIndex = Math.Min(_curIndex + 8, _pathLength); // "Infinity" has 8 characters
            simple = false;
        }
        // Check for NaN 
        else if (More() && (_pathString[_curIndex] == 'N'))
        {
            // 
            // Don't bother reading the characters, as the CLR parser will
            // do this for us later. 
            //
            _curIndex = Math.Min(_curIndex + 3, _pathLength); // "NaN" has 3 characters
            simple = false;
        }
        else
        {
            SkipDigits(!AllowSign);

            // Optional period, followed by more digits 
            if (More() && (_pathString[_curIndex] == '.'))
            {
                simple = false;
                _curIndex++;
                SkipDigits(!AllowSign);
            }

            // Exponent
            if (More() && ((_pathString[_curIndex] == 'E') || (_pathString[_curIndex] == 'e')))
            {
                simple = false;
                _curIndex++;
                SkipDigits(AllowSign);
            }
        }

        if (simple && (_curIndex <= (start + 8))) // 32-bit integer
        {
            int sign = 1;

            if (_pathString[start] == '+')
            {
                start++;
            }
            else if (_pathString[start] == '-')
            {
                start++;
                sign = -1;
            }

            int value = 0;

            while (start < _curIndex)
            {
                value = value * 10 + (_pathString[start] - '0');
                start++;
            }

            return value * sign;
        }
        else
        {
            string subString = _pathString.Substring(start, _curIndex - start);

            try
            {
                return System.Convert.ToDouble(subString, _formatProvider);
            }
            catch (FormatException except)
            {
                throw new FormatException(string.Format("Unexpected character in path '{0}' at position {1}", _pathString, _curIndex - 1), except);
            }
        }
    }

    private bool IsNumber(bool allowComma)
    {
        bool commaMet = SkipWhiteSpace(allowComma);

        if (More())
        {
            _token = _pathString[_curIndex];

            // Valid start of a number
            if ((_token == '.') || (_token == '-') || (_token == '+') || ((_token >= '0') && (_token <= '9'))
                || (_token == 'I')  // Infinity
                || (_token == 'N')) // NaN 
            {
                return true;
            }
        }

        if (commaMet) // Only allowed between numbers
        {
            ThrowBadToken();
        }

        return false;
    }

    private Point ReadPoint(char cmd, bool allowcomma)
    {
        double x = ReadNumber(allowcomma);
        double y = ReadNumber(AllowComma);

        if (cmd >= 'a') // 'A' < 'a'. lower case for relative
        {
            x += _lastPoint.X;
            y += _lastPoint.Y;
        }

        return new Point(x, y);
    }

    private bool ReadToken()
    {
        SkipWhiteSpace(!AllowComma);

        // Check for end of string 
        if (More())
        {
            _token = _pathString[_curIndex++];

            return true;
        }
        else
        {
            return false;
        }
    }

    private bool More()
    {
        return _curIndex < _pathLength;
    }

    // Skip white space, one comma if allowed
    private bool SkipWhiteSpace(bool allowComma)
    {
        bool commaMet = false;

        while (More())
        {
            char ch = _pathString[_curIndex];

            switch (ch)
            {
                case ' ':
                case '\n':
                case '\r':
                case '\t': // SVG whitespace 
                    break;

                case ',':
                    if (allowComma)
                    {
                        commaMet = true;
                        allowComma = false; // one comma only
                    }
                    else
                    {
                        ThrowBadToken();
                    }
                    break;

                default:
                    // Avoid calling IsWhiteSpace for ch in (' ' .. 'z']
                    if (((ch > ' ') && (ch <= 'z')) || !Char.IsWhiteSpace(ch))
                    {
                        return commaMet;
                    }
                    break;
            }

            _curIndex++;
        }

        return commaMet;
    }

    private void ThrowBadToken()
    {
        throw new FormatException(string.Format("Unexpected character in path '{0}' at position {1}", _pathString, _curIndex - 1));
    } 
    #endregion
}

XAML

<Border Grid.Row="3"
        Grid.Column="3"
        BorderBrush="Yellow"
        BorderThickness="1"
        Height="200"
        Width="200">
    <local:IconControl Height="200"
                       Width="200"
                       Padding="20"
                       Background="White"
                       Foreground="Red"
                       Data="{StaticResource PrancingString}" />
</Border>

The easiest way for me:

<ResourceDictionary ...>
    <Canvas x:Key="appbar_tools">
        <Path Fill="#FF000000" Data="F1 M 25.3333,42.75C 26.5189,42.75 27.6436,43.0106 28.6533,43.4777L 34.9459,37.185L 32.5825,34.8217L 30.3433,37.0609L 28.1042,34.8217L 29.0343,33.8915C 27.1425,33.1521 25.7233,31.6492 23.4735,29.3994C 18.836,24.7619 16.1846,19.8945 18.0395,18.0396C 19.8945,16.1846 23.9702,18.0444 28.6077,22.6819C 30.8575,24.9317 33.1521,27.1425 33.8915,29.0344L 34.8217,28.1042L 37.0608,30.3433L 34.8217,32.5825L 37.185,34.9459L 43.4777,28.6533C 43.0106,27.6436 42.75,26.5189 42.75,25.3333C 42.75,20.9611 46.2944,17.4167 50.6667,17.4167C 51.6877,17.4167 52.6636,17.61 53.5597,17.9619L 47.5,24.0216L 51.9783,28.5L 58.0381,22.4403C 58.39,23.3364 58.5833,24.3123 58.5833,25.3333C 58.5833,29.7056 55.0389,33.25 50.6667,33.25C 49.8136,33.25 48.9921,33.1151 48.2222,32.8654L 41.6634,39.4242L 50.8787,48.6395L 51.4384,48.0797L 56.8841,53.5253L 53.5253,56.8841L 48.0797,51.4384L 48.6395,50.8787L 39.4242,41.6634L 32.8654,48.2222C 33.1151,48.9921 33.25,49.8136 33.25,50.6667C 33.25,55.0389 29.7056,58.5833 25.3333,58.5833C 24.3123,58.5833 23.3364,58.39 22.4403,58.0381L 28.5,51.9783L 24.0217,47.5L 17.9619,53.5597C 17.61,52.6636 17.4167,51.6877 17.4167,50.6667C 17.4167,46.2944 20.9611,42.75 25.3333,42.75 Z "/>
    </Canvas>
</ResourceDictionary>

Then in your control:

<Rectangle Width="50" Height="50" Fill="Black">
    <Rectangle.OpacityMask>
        <VisualBrush Stretch="Fill" Visual="{DynamicResource appbar_copy}" />
    </Rectangle.OpacityMask>
</Rectangle>
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top