전체 페이지뷰

2017년 3월 8일 수요일

Chapter 6. Button Clicks, part 1

그래픽 사용자 인터페이스(GUI)는 정보 표시 프레젠테이션에 사용되는 view와, 사용자로부터 입력을 받는 interaction으로 크게 나눌 수 있습니다. Label을 가장 대표적인 프리젠테이션 view라고 한다면, Button은 아마도 전형적인 interaction일 것입니다. 사용자는 Button을 사용하여 프로그램에게 어떤 액션을 할 것을 명령합니다.
Xamarin.Forms Button은 텍스트(이미지는 있을수도 없을수도 있음. 이미지가 없는 것은 이번 장에서 설명하며 있는 것은 Ch.13 Bitmaps에서 설명합니다)를 표시합니다. 사용자의 손가락이 버튼을 터치하면, 상호작용 하고 있다는 피드백을 주기 위해 모양이 바뀝니다. 손가락이 떼지면 Clicked 이벤트가 발생하는데, 거기에 전달되는 두 인수는 Xamarin.Forms의 이벤트 핸들러에 전형적으로 쓰이는 것들입니다.


  • 첫 번째 인수는 이벤트를 일으키는 object입니다. Clicked 핸들러의 경우에, 이것은 눌려진  Button 오브젝트가 됩니다.
  • 두 번째 인수는 이벤트에 대한 자세한 정보를 제공하기도 하는데, Clicked 이벤트의 경우엔  추가 정보를 제공하지 않는 단순한 EventArgs 객체입니다.

앱이 사용자 UI를 구현하기 시작하면 몇 가지 요구 사항이 발생합니다: 사용자가 작업을 완료하기 전에 프로그램이 종료되는 경우 해당 interaction 결과를 저장해야 합니다. 따라서 이 장에서는 응용 프로그램이 일시적인 데이터를 저장하는 방법, 특히 응용 프로그램 라이프 사이클 이벤트에 대해서도 설명합니다.

Processing the click

다음은 ButtonLogger라는 프로그램을 소개하겠습니다. 이 프로그램에는 하나의 큰 Stacklayout이 존재하며 그것을 ButtonScrollView가 공유하고 있습니다. 그리고 ScrollView 내에 또 하나의 StackLayout이 존재하는 구조입니다. Button이 클릭될 때마다 스크롤 가능한 StackLayout내에 로그기록을 남기는 Label이 추가됩니다.

public class ButtonLoggerPage : ContentPage
{
    StackLayout loggerLayout = new StackLayout();
    public ButtonLoggerPage()
    {
        // Create the Button and attach Clicked handler.
        Button button = new Button
        {
            Text = "Log the Click Time"
        };
        button.Clicked += OnButtonClicked;
        this.Padding = new Thickness(5, Device.OnPlatform(2000), 50);
        // Assemble the page.
        this.Content = new StackLayout
        {
            Children =
            {
                button,
                new ScrollView
                {
                    VerticalOptions=LayoutOptions.FillAndExpand,
                    Content=loggerLayout
                }
            }
        };
    }
    void OnButtonClicked(object sender, EventArgs args)
    {
        // Add Label to scrollable Stacklayout.
        loggerLayout.Children.Add(new Label
        {
            Text = "Button clicked at " + DateTime.Now.ToString("T")
        });
    }
}
cs

이 책에서 모든 이벤트 핸들러는 On으로 시작하고 그 다음에 이벤트를 발생시키는 view의 이름, 다음에 이벤트의 순으로 명명된다는 것을 알아두시기 바랍니다. 결과적으로 On+Button+Clicked = OnButtonClicked 가 됩니다.

생성자는 Button을 만든 직후 Clicked 핸들러를 Button에 연결합니다. 그런 다음 이 페이지는 Button과, loggerLayout이라는 이름의 또 다른 StackLayout을 가지는 ScrollView로 어셈블됩니다. ScrollViewVerticalOptionsFillAndExpand로 설정되었기 때문에 ButtonStackLayout을 공유하면서도 화면에 보이고 스크롤도 가능해집니다.

클릭을 몇번 한 결과를 보여드리겠습니다.

보시다시피 버튼의 모양이 플랫폼마다 다릅니다. 각 플랫폼에 맞게 렌더링된 결과인데, iOS에선 UIButton, 안드로이드에선 안드로이드용 Button, 윈도우즈10 모바일에서는 WinRT용 Button입니다. 버튼은 차지하는 영역을 채우고 가운데에 텍스트가 써지는 것이 default입니다.

Button은 모양을 바꿀 수 있는 몇가지 속성을 정의합니다:

  • FontFamily (type은 string)
  • FontSize(type은 double)
  • FontAttributes (type FontAttributes)
  • TextColor (type Color, default는 Color.Default)
  • BorderColor (type Color, default는 Color.Default)
  • BorderWidth (type double, default는 0)
  • BorderRadius (type double, default는 5)
  • Image (Chapter 13에서 설명 예정)

또한 ButtonVisualElement로부터 BackgroundColor 속성 (물론 그외에도 여러가지를 상속받습니다)을 상속하고 View에서 HorizontalOptionsVerticalOptions를 상속합니다.

일부 Button 속성은 플랫폼마다 약간 다르게 작동할 수 있습니다.  스크린 샷의 어떤 버튼에도 테두리가 없습니다(Windows Phone 8.1의 버튼에는 하얀 테두리가 보이는것이 default입니다). 그러나 BorderWidth 속성을 0이 아닌 값으로 설정하면, iPhone에는 테두리가 검정색으로 표시됩니다. 또, BorderColor 속성을 Color.Default 이외의 값으로 설정하면 테두리가 Windows 10 Mobile 장치에서만 표시됩니다. 그러므로 iOS와 Windows 10 모바일 장치 모두에서 눈에 보이는 경계선을 원하면 BorderWidthBorderColor를 모두 설정하면 됩니다.  그러나 BackgroundColor 속성까지 설정하지 않으면 안드로이드 장치에는 테두리가 표시되지 않습니다. 버튼 테두리를 custom 정의할 때에는 Device.OnPlatform을 사용하는 것이 유용합니다 (10 장, "XAML markup extensions"참조).

BorderRadius 속성은 테두리의 날카로운 모서리를 둥글리기 위한 것이며 경계가 표시되면 iOS 및 Android에서는 작동하지만 Windows 10 및 Windows Mobile에서는 작동하지 않습니다. BorderRadius는 Windows 8.1 및 Windows Phone 8.1에서 작동하지만 BackgroundColor와 함께 사용하면 배경이 테두리 밖으로 튀어나옵니다.

ButtonLogger와 비슷하지만 LoggerLayout 객체를 필드로 저장하지 않는 프로그램이 있다고 가정해 봅시다. Clicked 이벤트 핸들러 내에서 해당 StackLayout 객체에 액세스 할 수 있겠습니까?

Walking the visual tree 라는 기술을 사용하면 그렇게 하는 것이 가능해집니다. OnButtonClicked 핸들러에 대한 sender 인수는 이벤트를 발생시키는 객체 (이 경우 Button)이므로 해당 인수를 캐스팅하여 Clicked 핸들러를 시작할 수 있습니다:

Button button = (Button)sender;
cs

ButtonStackLayout의 자식이므로 Parent 속성에서 해당 개체에 액세스 가능합니다. 한번 더 캐스팅이 필요해 집니다:

StackLayout outerLayout = (StackLayout)button.Parent;
cs

StackLayout의 두 번째 자식은 ScrollView이므로 Children 속성은 다음과 같이 인덱싱 할 수 있습니다.

ScrollView scrollView = (ScrollView)outerLayout.Children[1];
cs

ScrollViewContent 속성이 바로 찾고 있던 StackLayout입니다

물론 향후 레이아웃이 변경되거나 tree-walking 코드를 변경하는 것을 잊어버렸을 때 더 이상 코드가 유효하지 않다는 단점이 있으므로, 페이지를 어셈블하는 코드와 해당 페이지의 뷰에서 이벤트를 핸들링하는 코드를 분리시키는 편이 좋습니다.


Sharing button clicks


프로그램에 다수의 Button이 있으면 그 각각이 Clicked 핸들러를 가질수 있습니다. 그러나 때로는 그 다수의 버튼이 하나의 공통 Clicked 핸들러를 공유하는 것이 좋을 때가 있습니다.

계산기 프로그램을 생각해 봅시다. 0에서 9까지 써있는 버튼은 기본적으로 다 같은 것이고, 이 10개에 대해 10개의 Clicked 핸들러를 작성하는 것은 합리적이지 못한 일입니다.

Clicked 핸들러의 첫 번째 인수를 Button 타입의 객체로 캐스팅 할 수 있는 방법은 이미 살펴 보았습니다. 그런데 어떤 Button이 눌렸는지는 어떻게 판정할까요?

첫번째 방법은 모든 Button 객체를 필드로 저장하여 어떤 Button이 이벤트를 발생시킨 것인지 비교해 보는 방법입니다.

TwoButtons라는 프로그램으로 이 방법을 보여 드리겠습니다. 이 프로그램은 버튼이 한 개 있던 바로 전의 프로그램과 유사하지만 Button이 두개이며 하나는 StackLayoutLabel을 더하는 용도이고, 다른 하나는 제거용입니다. 두 Button은 필드로 저장되어 어떤것이 Clicked 이벤트를 일으켰는지 알수 있게 해 줍니다.

public class TwoButtonsPage : ContentPage
{
    Button addButton, removeButton;
    StackLayout loggerLayout = new StackLayout();
    public TwoButtonsPage()
    {
        // Create the Button views and attach Clicked handler
        addButton = new Button
        {
            Text = "Add",
            HorizontalOptions = LayoutOptions.CenterAndExpand
        };
        addButton.Clicked += OnButtonClicked;
        removeButton = new Button
        {
            Text = "Remove",
            HorizontalOptions = LayoutOptions.CenterAndExpand,
            IsEnabled = false
        };
        this.Padding = new Thickness(5, Device.OnPlatform(2000), 50);
        // Assemble the page
        this.Content = new StackLayout
        {
            Children =
            {
                new StackLayout
                {
                    Orientation= StackOrientation.Horizontal,
                    Children=
                    {
                        addButton,
                        removeButton
                    }
                },
                new ScrollView
                {
                    VerticalOptions=LayoutOptions.FillAndExpand,
                    Content=loggerLayout
                }
            }
        };                    
    }
    void OnButtonClicked(object sender, EventArgs args)
    {
        Button button = (Button)sender;
        if (button == addButton)
        {
            // Add Label to Scrollable StackLayout
            loggerLayout.Children.Add(new Label
            {
                Text = "Button Clicked at " + DateTime.Now.ToString("T")
            });
        }
        else
        {
            // Remove topmost Label from StackLayout
            loggerLayout.Children.RemoveAt(0);
        }
        // Enable "Remove" button only if children are present
        removeButton.IsEnabled = loggerLayout.Children.Count > 0;
    }
}
cs

두 버튼 모두 HorizontalOptions 값이 CenterAndExpand이므로, StackLayout을 사용하여 화면 상단에 나란히 표시됩니다.


Clicked 핸들러가 removeButton을 감지하면 Children 속성에 RemoveAt 메서드를 호출합니다.

loggerLayout.Children.RemoveAt(0);

그러나 혹 children이 없다면 RemoveAt이 예외를 발생시키지는 않을까요?

그런 일은 발생하지 않습니다. TwoButtons 프로그램이 시작될 때 removeButtonIsEnabled 속성이 false로 초기화 되었기 때문입니다. 이렇게 disable 시키면 버튼이 흐리게 보이면서 사용자로 하여금 기능하지 않는다는 것을 알게 해주며 Clicked 이벤트가 발생하지 않습니다. Clicked 핸들러가 끝날 때까지 이 IsEnabled 속성은 loggerLayout에 하나 이상의 child가 있는 경우에만 true로 설정됩니다.

이 사례에서 알 수 있는 규칙 하나 : 코드가 버튼 Clicked 이벤트가 유효한지 판정 해야하는 경우 버튼을 비활성화하여 잘못된 버튼 클릭을 방지하는 것이 좋습니다.


Anonymous event handlers

Clicked 핸들러를 무명의 람다 함수로 작성할 수도 있습니다. 숫자를 표시하는 Label 하나와 두개의 Button으로 이루어진 ButtonLambdas라는 프로그램을 보여 드리겠습니다. 하나의 버튼은 곱하기 2를 하며 다른 하나는 나누기 2를 합니다. 보통, 숫자와 Label 변수는 필드로 저장되고, 이 변수가 정의된 직후에 생성자 내에서 익명 람다 이벤트 핸들러가 정의되므로 이벤트 핸들러가 로컬 변수에 직접 접근할 수 있습니다.

public class ButtonLambdasPage : ContentPage
{
    public ButtonLambdasPage()
    {
    // Number to manipulate
        double number = 1;
 
        // Create Label for display.
        Label label = new Label
        {
            Text = number.ToString(),
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)),
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.CenterAndExpand
        };
 
        // Create the first Button and attach Clicked handler.
        Button timesButton = new Button
        {
            Text = "Double",
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Button)),
            HorizontalOptions = LayoutOptions.CenterAndExpand
        };
        timesButton.Clicked += (sender, args) =>
        {
            number *= 2;
            label.Text = number.ToString();
        };
 
        // Create the second Button and attach Clicked handler.
        Button devideButton = new Button
        {
            Text = "Half",
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Button)),
            HorizontalOptions = LayoutOptions.CenterAndExpand
        };
        devideButton.Clicked += (sender, args) =>
        {
            number /= 2;
            label.Text = number.ToString();
        };
 
        // Assemble the page
        this.Content = new StackLayout
        {
            Children =
            {
                label,
                new StackLayout
                {
                    Orientation=StackOrientation.Horizontal,
                    VerticalOptions=LayoutOptions.CenterAndExpand,
                    Children=
                    {
                        timesButton,
                        devideButton
                    }
                }
            }
        };
    }
}
cs

Device.GetNamedSize를 사용하여 LabelButton 모두 큰 폰트로 설정합니다. Label과 함께 사용되었을 때 GetNamedSize의 두번째 인수는 Label이며, Button의 경우에는 두 번째 인수가 Button이 되는데 그 크기는 다를 수 있습니다.
이 전의 프로그램처럼 두개의 버튼은 하나의 수평 StackLayout을 공유합니다.


람다 이벤트 핸들러를 사용하는 것에 있어 단점은, 여러 개의 뷰에서 그 핸들러를 공유할 수 없다는 점입니다(하려고 하면 할수도 있지만 복잡한 코드가 필요합니다).



댓글 없음:

댓글 쓰기